Angular material checkbox automatically un-checks itself - html

I have a list that I display as checkboxes using angular-material (Angular 7). Below I will add code snippet for .html and .ts files.
Whenever I click on a checkbox it is checked but then immediately un-checked. I entered in debug mode and see that when I click on a checkbox, my isSelected() method gets called 4 times by Angular. When I click on it, it immediately goes to checked state. Then it is still checked the second time that Angular calls it. On the third time, it becomes un-checked (meanwhile isSelected() is still true). I cannot figure out what I did wrong. What I tried is:
Switch from isSelected() method to a class property (added the isSelected boolean field on myListItem objects)
Added bidirectional binding on top of the previous idea
Switch from checked to ngModel
Nothing helped. What else to try, I don't know. Please help me out.
html snippet:
class MyListItem {
id: number
name: string
}
// omitted annotations
export class MyComponent implements OnInit, OnDestroy {
myList: MyListItem[] = [] // omitted initialization
isSelected(myListItem: MyListItem): boolean {
return this.myList.includes(myListItem)
}
toggle(myListItem: MyListItem): void {
// omitted the code, I debugged it and it works correctly:
// it adds/removes the item to/from the list
}
}
<mat-list>
<mat-list-item *ngFor="let myListItem of myList">
<mat-checkbox flex="100" (click)="toggle(myListItem)"
[checked]="isSelected(myListItem)">
{{ myListItem.name }}
</mat-checkbox>
</mat-list-item>
</mat-list>

Use change event not click:
<mat-checkbox flex="100" (change)="toggle(myListItem)"
[checked]="isSelected(myListItem)">
{{ myListItem.name }}
</mat-checkbox>

I am not sure if this will work but you can add an Event parameter to the toggle function.
toggle(myListItem: MyListItem, event: any) { event.preventDefault() }
Then in your html:
(click)="toggle(myListItem, $event)"
Again, Not sure if this will work, but I have found that sometimes these click events will happen automatically, unless the prevent default() function is called

Related

Is it possible to use [maxSelectedLabels] property in an ngif condition?

I'm using Prime NG Multiselect component and I want to show selectedItemsLabel="{0} Selected" when there are more than 3 selected checkboxes, but if all of the checkboxes are selected, then selectedItemsLabel="All" should be shown in the placeholder.
I'm new to angular and I been following documentation of this MultiSelect component, yet this doesn't show the options to able to implement multiple conditions of properties, and I was wondering if it's even possible.
Example of how It might be
<ng-template pTemplate="filter" let-value let-filter="filterCallback">
<p-multiSelect
[ngModel]="value"
[options]="routeOptions"
placeholder="Any"
(onChange)="filter($event.value)"
optionLabel="name"
selectedItemsLabel="{0} selected"
[maxSelectedLabels]="3"
>
<ng-template let-option pTemplate="item">
<div>
<span class="p-ml-1">{{ option.name }}</span>
</div>
<div *ngIf="[maxSelectedLabels="routeOptions.length - 1"] Then selectedItemsLabel="All"></div>
</ng-template>
</p-multiSelect>
</ng-template>
Yes, you can. First give the component a ref with # like this:
<p-multiSelect
#myMultiSelect
[ngModel]="value"
[options]="routeOptions"
placeholder="Any"
(onChange)="filter($event.value)"
optionLabel="name"
selectedItemsLabel="{0} selected"
[maxSelectedLabels]="3"
>
.......
Then you have access to it:
<div *ngIf="myMultiSelect.maxSelectedLabels === routeOptions.length - 1">Im visible</div>
If the option of maxSelectedLables is the length - 1 of routeOptions then the div is visible. That is how ngIf works
BUT
Thats not what you want. You wanna set the selectedItemsLabel property. And you have it not understand correctly. You set the maxSelectedLables to 3 as example AND set the selectedItemsLabel directly, too! The text of the selectedItemsLabel will be only shown if needed (controlled by the component).
<h5>Basic</h5>
<p-multiSelect #meins [options]="cities" [(ngModel)]="selectedCities" defaultLabel="Select a City" optionLabel="name"
[maxSelectedLabels]="3" selectedItemsLabel="{0} items selected">
</p-multiSelect>
Look here the Stackblitz!
The documentation of ng-prime will helps, too and say:
selectedItemsLabel: Label to display after exceeding max selected labels e.g. ({0} items selected), defaults "ellipsis" keyword to indicate a text-overflow.
UPDATE 18.02.2023
You wanna show "ALL" only if all items selected. So add the onChange event and bind the selectedItemsLabel. Why binding? It has some problems with a condition in it. So we make it inside the code.
HTML
<p-multiSelect [options]="cities" [(ngModel)]="selectedCities" defaultLabel="Select a City" optionLabel="name"
[maxSelectedLabels]="2" [selectedItemsLabel]="bindTest" (onChange)="onChange()">
</p-multiSelect>
Inside the code do the follow with onChange:
Code
onChange() {
if (this.selectedCities.length === this.cities.length) {
this.bindTest = "ALL";
this.changeRef.detectChanges();
}
else {
this.bindTest = "{0} items selected";
this.changeRef.detectChanges();
}
}
Now it works how you wish. One important thing: We use changeRef.detectChanges(); Without this the components selected text will not changing directly. Import it in the components constructor:
constructor(
private countryService: CountryService,
private primengConfig: PrimeNGConfig,
private changeRef: ChangeDetectorRef
) {
.....
I made a Stackblitz of the problem: https://stackblitz.com/edit/primeng-tablefilter-demo-ipt7y1?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.component.ts
(Expand the page to the left to view the column filter in the stackblitz)
If you notice, the clear button doesn't clear the selected textbox anymore. After some testing it seems the [(ngModel)] breaks it, I think it got to do something with two-way binding? It is not shown in the stackblitz, but if you include
(onChange)="filter($event.value)"
the clear button still clears the filter from the table, but not in the selected textbox.
I found out that there is this property
[showClear]="true"
That adds an X at the end of the textbox that clears it out. Sadly, the styling/positioning is not what I need.
What could be the ways to fix the clear button ? Add a ts function to clear out the selected values? If so, how to bind it to the clear button because it is generated from
<p-columnFilter
display="menu"
menu property and I had no luck to find the way to try add/change functionality to that button.

ngClass not changing on Route Event

i have a bottomNavigation that tells in which route we are clicking by changing theyr style.
Here is my template of the bottom-navigation template
class="menu-icon"
[ngClass]="{ 'active-item': buttonActivated.value == '/my-goal'}"
[ngClass]="{ 'active-item': buttonActivated.value == '/my-goal/goal-detail'}"
>
Here is my TS file
buttonActivated: BehaviorSubject<string> = new BehaviorSubject<string>('/')
constructor(private router: Router) {
this.router.events.subscribe(() => {
this.buttonActivated.next(this.router.url);
} }
So the logic is: everytime the route changes or have an event, the buttonActivated.value will change.
And depending on that value the ngClass would change to active-item.
When i click to /my-goal route the ngClass works perfectly, but when i click in his child /my-goal/goal-detail/id and when i come back in the same route, the ngClass is not working as a active-item.
I checked the value of buttonActivated.value and is changing correctly when i click back on the parent route, but the ngClass is still not changing.
Thank you in advance
You can use routerLinkActive directive.
RouterLinkActive directive add the .active class by tracking either the linked route of element is currently active or not, and then allow to specify CSS as per needed.

Angular. Getting wrong data from ngb-panel

I use ngb-accordion in my app. I am trying to get data from every panel but when the first panel is opened click from the second panel returns me wrong data.
Result
I think the problem is the event which raises when input file changes.
Stackblitz Link
I will be glad if someone give me a hint for solving this problem.
There are few things to note in your code.
Your *ngFor is at ngb-accordion which is creating a new accordion for every loop, instead of creating multiple panel within one accordion.
Fix: <ngb-panel *ngFor="let data of datalist; let i = index">
You are using the same label for all three panels, because of which your first panel is opening every time, regardless of which panel you are clicking.
Fix: <label [for]="'image-input-' + i"> and <input ... [id]="'image-input-' + i"
The modal that opens after image selection has no knowledge of which panel it's getting triggered from. So, you have to use your (change)="onFileChange($event, data)" event/function to keep track of selected panel/corresponding data.
Then you can pass that selection from your modal to your processFile(...)
Fix:
export class AppComponent {
...
selectedData: Data;
...
...
onFileChange(event: any, data): void {
...
this.selectedData = data;
}
}
html:
...
<input ... (change)="onFileChange($event, data)>
...
...
<button
...
(click)="processFile(imageInput, selectedData)"
> Done
</button>
Stackblitz Demo

Get selected tab to switch icons Ionic 5

I'm trying to change my tab icons from filled to outline when someone selects it (filled when selected, outline when not selected).
On the Ionic 5 Tabs Doc there's a getSelected() method but no examples on how to use this.
My idea was to use ionTabsDidChange to detect when someone clicked a tab, then use getSelected() and set the icon from 'home' to 'home-outline'.
Tabs.html
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button class="tab-btn" tab="home">
<ion-icon name="{{ homeIcon }}"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button class="tab-btn" tab="price">
<ion-icon name="{{ priceIcon }}"></ion-icon>
<ion-label>Price Search</ion-label>
</ion-tab-button>
<ion-tabs>
Tabs.ts
export class TabsPage {
public homeIcon = 'home';
private homeFilled = 'home';
private homeOutline = 'home-outline'
public priceIcon = 'search-outline';
private priceFilled = 'search';
private priceOutline = 'search-outline';
ionTabsDidChange() {
let selectedTabName = getSelected();
// Stuff to switch icon from filled to outline and vice versa
}
}
The issue is that I don't know how to use getSelected(), I've tried adding ViewChild like this stackoverflow, but getSelected() is not a function (Changed to IonTabs because Tabs don't exist in Ionic 5.
At this point, the only solution I can think of is saving the tabs state and adding click functions for everything.
You are heading the right direction, there are still few missing points. In the Ionic doc you are reading the "events" are not directly accessible in the page without binding them to the component itself and in order to use ViewChild you also need to give the component an id:
Tabs.html
<ion-tabs #tabs (ionTabsDidChange)="setCurrentTab()">
"tabs" will be the id of the component and whenever ionTabsDidChange event gets triggered it will call setCurrentTab method, it should be declared on your page.
Then in the page, as you have already mentioned you'll need to add a ViewChild (now possible with the id) and use getSelected() method.
Tabs.ts
export class TabsPage {
#ViewChild('tabs', { static: false }) tabs: IonTabs;
...
setCurrentTab() {
this.selectedTab = this.tabs.getSelected();
}
}
And voila, that should be it :-)
There's another really easy way to do this. First add the ionTabsDidChange attribute to your ion-tabs element as #andriyleu suggests but this time make sure to pass $event details in.
<ion-tabs (ionTabsDidChange)="setCurrentTab($event)">
Then you can define the function in your ts file like this.
current_tab = "home"; // Set this as you default page name
setCurrentTab(ev: any){
this.current_tab = ev.tab;
}
Finally, in your html you can use a very efficient piece of Javascript to determine which icon to show. Perhaps like me, you're switching between a filled and outline version.
<ion-icon [name]="current_tab == 'home' ? 'home' : 'home-outline'"></ion-icon>
Thanks to the everyone who answered this and helped me figure it out!
Inside the ion-tabs tag, ionTabsDidChange passes an event which has the selected tab. You can get that event by doing the following then it should give you the clicked tab:
tabs.html
<ion-tabs (ionTabsDidChange)="tabClicked($event)">
tabs.ts
tabClicked(e) {
this.selectedTab = e.tab
}
Doesn't work in React.
const log = (e: any): any => {
console.log(e);
}
<IonTabs ionTabsDidChange={log}>
Type '{ children: Element[]; ionTabsDidChange: (e: any) => any; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<IonTabs> & Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
[react-scripts] Property 'ionTabsDidChange' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<IonTabs> & Readonly<Props> & Readonly<{ children?: ReactNode; }>'. TS2322
[react-scripts] 47 | <IonApp>
Any thoughts?
Ionic will add "tab-selected" class to the selected tab. You can use that class to style the tab icon.
This post is old and answered but I'm going to expand Shrishail's answer, because I think it's the better answer and it didn't get enough attention. when you just want to change looks, like changing icon, it's better to use css. here is a working example based on tab-selected class:
<ion-tab-button>
<ion-icon name="home" class="selected"></ion-icon>
<ion-icon name="home-outline" class="unselected"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
and in css:
.tab-selected .unselected {
display: none;
}
.tab-selected .selected {
display: initial !important;
}
.selected {
display: none;
}
For people that use react.js, this solution was effective for me.
const App: React.FC = () => {
const [selectedTab, setSelectedTab] = useState<string>();
const handleTabsDidChange = async (event: CustomEvent) => {
setSelectedTab(event.detail.tab);
};
return (
<IonApp>
<IonReactRouter>
<IonTabs onIonTabsDidChange={handleTabsDidChange}>
<IonRouterOutlet>
<Route exact path="/status">
<Status />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="status" href="/status">
<IonIcon icon={selectedTab === 'status' ? disc : discOutline} />
<IonLabel>Status</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
};

Angular 5 FormGroup reset doesn't reset validators

I have a form on my page and when I call FormGroup.reset() it sets the forms class to ng-pristine ng-untouched but FormControl.hasError(...) still returns truthy. What am I doing wrong here?
Template
<form [formGroup]="myForm" (ngSubmit)="submitForm(myForm)">
<mat-form-field>
<input matInput formControlName="email" />
<mat-error *ngIf="email.hasError('required')">
Email is a required feild
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="password.hasError('required')">
Password is a required feild
</mat-error>
</mat-form-field>
<button type="submit">Login</button>
</form>
Component
export class MyComponent {
private myForm: FormGroup;
private email: FormControl = new FormContorl('', Validators.required);
private password: FormControl = new FormControl('', Validators.required);
constructor(
private formBuilder: FormBuilder
) {
this.myForm = formBuilder.group({
email: this.email,
password: this.password
});
}
private submitForm(formData: any): void {
this.myForm.reset();
}
}
Plunker
https://embed.plnkr.co/Hlivn4/
It (FormGroup) behaves correctly. Your form requires username and password, thus when you reset the form it should be invalid (i.e. form with no username/password is not valid).
If I understand correctly, your issue here is why the red errors are not there at the first time you load the page (where the form is ALSO invalid) but pop up when you click the button. This issue is particularly prominent when you're using Material.
AFAIK, <mat-error> check the validity of FormGroupDirective, not FormGroup, and resetting FormGroup does not reset FormGroupDirective. It's a bit inconvenient, but to clear <mat-error> you would need to reset FormGroupDirective as well.
To do that, in your template, define a variable as such:
<form [formGroup]="myForm" #formDirective="ngForm"
(ngSubmit)="submitForm(myForm, formDirective)">
And in your component class, call formDirective.resetForm():
private submitForm(formData: any, formDirective: FormGroupDirective): void {
formDirective.resetForm();
this.myForm.reset();
}
GitHub issue: https://github.com/angular/material2/issues/4190
In addition to Harry Ninh's solution, if you'd like to access the formDirective in your component without having to select a form button, then:
Template:
<form
...
#formDirective="ngForm"
>
Component:
import { ViewChild, ... } from '#angular/core';
import { NgForm, ... } from '#angular/forms';
export class MyComponent {
...
#ViewChild('formDirective') private formDirective: NgForm;
constructor(... )
private someFunction(): void {
...
formDirective.resetForm();
}
}
After reading the comments this is the correct approach
// you can put this method in a module and reuse it as needed
resetForm(form: FormGroup) {
form.reset();
Object.keys(form.controls).forEach(key => {
form.get(key).setErrors(null) ;
});
}
There was no need to call form.clearValidators()
Add the property -
#ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
and use this instead of this.myForm.reset();
this.formGroupDirective.resetForm();
This will reset the error display and also do the job of form.reset(). But the form, along with the fields, will still show ng-invalid class
Check this answer for more details - https://stackoverflow.com/a/56518781/9262627
The below solution works for me when trying to reset specific form controller in form group -
this.myForm.get('formCtrlName').reset();
this.myForm.get('formCtrlName').setValidators([Validators.required, Validators.maxLength(45), Validators.minLength(4), Validators.pattern(environment.USER_NAME_REGEX)]);
this.myForm.get('formCtrlName').updateValueAndValidity();
form.reset() won't work on custom form control like Angular Material that's why the function is not working as expected.
My workaround for this is something like this
this.form.reset();
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
this.form.reset() the issue with this is that it will reset your formcontrol values but not the errors so you need to reset them individually by this line of code
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
With this you don't need to use FormGroupDirective which is a cleaner solution for me.
Github issue: https://github.com/angular/angular/issues/15741
I found that after calling resetForm() and reset(), submitted was not being reset and remained as true, causing error messages to display. This solution worked for me. I found it while looking for a solution to calling select() and focus() on an input tag, which also wasn't working as expected. Just wrap your lines in a setTimeout(). I think setTimeout is forcing Angular to detect changes, but I could be wrong. It's a bit of a hack, but does the trick.
<form [formGroup]="myFormGroup" #myForm="ngForm">
…
<button mat-raised-button (click)="submitForm()">
</form>
submitForm() {
…
setTimeout(() => {
this.myForm.resetForm();
this.myFormGroup.reset();
}, 0);
}
resetForm() {
this.myFormGroup.reset();
this.myFormGroup.controls.food.setErrors(null);
this.myFormGroup.updateValueAndValidity();
}
UPDATE FROM 2021 - ANGULAR 11.2
The fact to use a [formGroup]="form and a #formDirective="ngForm" directly into the HTML function is not a good practise. Or maybe you would prefer to use #ViewChild, and do it directly from your .ts. Actually, the problem don't come from Angular, but Material.
If you take a look at their GitHub, you will see this :
/** Provider that defines how form controls behave with regards to displaying error messages. */
#Injectable({providedIn: 'root'})
export class ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
}
}
The form will keep its submitted state. So you just have to delete the last part of the function.
Here is my solution (tested and working). I have a Material Module, into I've implemented this :
export class ShowOnInvalidTouchedErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl): boolean {
return !!(control && control.invalid && control.touched);
}
}
#NgModule({
providers: [
{
provide: ErrorStateMatcher, useClass: ShowOnInvalidTouchedErrorStateMatcher
}
],
exports: [
MatSnackBarModule,
MatTabsModule,
...
]
});
If you want to use this ErrorStateMatcher on only one form, it's possible. Please see this Material example. This is the same principle.
I had no luck with resetting the form directive. But You can also change the input state to pending to do that as well.
this.myForm.get("email").reset();
this.myForm.get("password").reset();
To anyone whom this may help, I am running Angular 9.1.9 and I didn't want to reset the form/controls just the overall validity of the form so I just ran:
this.registerForm.setErrors(null);
...where registerForm: FormGroup and that reset the form errors, leading to:
this.registerForm.valid
...returning true.
The same can be done for controls:
this.registerForm.get('email').setErrors(null)
As soon as the form is touched, these errors are re-evaluated anyway so if that's not good enough, you may need to have a boolean flag to further pin-down exactly when you want to start showing/hiding error UI.
I did not need to touch the directive in my case.
I was also having the same set of problems. My problem was that i was using mat-form-field and formGroup. After resetting the form submitted flag was not resetting.
So, the solution that worked for me is, putting a directive of ngForm along with formGroup and passing onSubmit(form). Added
#ViewChild('form') form;
in component and then I used
this.form.resetForm();
Nothing from above worked for me (Angular 7.2, Angular Material 7.3.7).
Try to pass with submit method an event on view:
<form [formGroup]="group" (ngSubmit)="onSubmit($event)">
<!-- your form here -->
</form>
Then use it to reset currentTarget and your form afterwards:
public onSubmit(event): void {
// your code here
event.currentTarget.reset()
this.group.reset()
}
Simple fix: use button with type="reset" and function submitForm() together
<form [formGroup]="MyForm" (ngSubmit)="submitForm()">
<input formControlName="Name">
<mat-error>
<span *ngIf="!tunersForm.get('Name').value && tunersForm.get('Name').touched"></span>
</mat-error>
<button type="reset" [disabled]="!MyForm.valid" (click)="submitForm()">Save</button>
</form>