Angular 2 - Checkbox not kept in sync

2019-01-15 17:15发布

问题:

Im working on Angular 2.0 (Yes, a bit behind from current 2.4).

I have a checkbox list. Im trying to make that when the LAST CHECKBOX IS UNCHECKED all CHECKBOXES ARE CHECKED.

HTML

<div *ngFor="let filter of filters">
    <label htmlFor="check-box-{{filter.text}}"
           [ngClass]="(filter.selected)? 'active' : '' ">

           <input type="checkbox" 
                 name="check-box-{{filter.text}}"
                 [checked]="filter.selected"
                 (change)="onSelectFilter(filter)"
                 attr.id="check-box-{{filter.text}}">

           {{filter.selected}} -  ({{filter.counter}})

    </label>
</div>

TS

onSelectFilter(filter: Filter){
    filter.toggleSelection();


    let isAnyFilterSelected = this.filters.find(filter => filter.selected);

    // If no filter is selected then ALL filters are selected
    if(!isAnyFilterSelected){
        this.filters.forEach(filter => filter.selected = true );
    }

}

I create a plunker for it.

https://plnkr.co/edit/uzF6Lk5fxRZjXBOaS9ob?p=preview

If unchecked the only checkbox with CHECKED attribute TRUE, Im expecting that ALL checkboxes would have CHECKED attribute. This does not happen.

Any ideas?

回答1:

You should use ngModel instead of binding to checked, and use a setTimeout.

<div *ngFor="let filter of filters">
    <label htmlFor="check-box-{{filter.text}}"
           [ngClass]="(filter.selected)? 'active' : '' ">

           <input type="checkbox" 
                 name="check-box-{{filter.text}}"
                 [ngModel]="filter.selected"
                 (ngModelChange)="onSelectFilter($event,filter)"
                 attr.id="check-box-{{filter.text}}">

           {{filter.selected}} -  ({{filter.counter}})

    </label>
</div>
onSelectFilter(selected:boolean,filter: Filter){
    filter.selected=selected;
    if(this.filters.every(filter=>!filter.selected)){
      setTimeout(()=>{
        this.filters.forEach(filter=>filter.selected=true);
      });
    }
}

plunkr

"why such a fishy trick is needed ?"

Actually, because from change-detector's point of view, there is no change between the previous state and the new one.

So there is no need to update the @Input() of the child /call the writeValue() method of the ControlValueAccessor (<input type="checkbox" [ngModel]="foo">).

Using setTimeout, you first update the property to false, then delay its change back to initial state, allowing a new change-detection cycle.

Also note that events like (ngModelChange) are not correlated to the change-detection cycle.

Without setTimeout() :

Here, we will get the same result as in your example, while we keep foo being true, the checkbox won't update :

The code (plunkr):

@Component({
  selector: 'my-app',
  template: `
    <input id="foo" type="checkbox" [ngModel]="foo" (ngModelChange)="fooChange($event)"><label for="foo">{{foo}}</label>
  `,
})
export class App {
  filters:[];
  foo=true
  fooChange(newValue:boolean){
    if(newValue===false)
        this.foo=true; // if newValue is false, foo becomes true
      else
        this.foo = newValue; // otherwise, do change
  }
}

What is happening under the hood :

With setTimeout()

This time we will delay resetting the value to a next tick using setTimeout :

The code (plunkr):

@Component({
  selector: 'my-app',
  template: `
    <input id="foo" type="checkbox" [ngModel]="foo" (ngModelChange)="fooChange($event)"><label for="foo">{{foo}}</label>
  `,
})
export class App {
  filters:[];
  foo=true
  fooChange(newValue:boolean){
    this.foo=newValue; // we need to make it change !
    setTimeout(()=>{
      if(newValue===false)
        this.foo=true; // if newValue is false, foo becomes true
      else
        this.foo = newValue; // otherwise, do change  
    })
  }
}

What is happening under the hood :