3

I have a component which displays a list of 'items' which are components created with a selector. I have a checkbox which i want, when clicked to update the 'state' of all child components.

Im really struggling to find the correct solution for doing this. Please see Plunkr for more info.

//our root app component
import {Component, EventEmitter} from 'angular2/core'

class Item {
  name: boolean;

  constructor(name: string) {
    this.name = name;
  }
}

@Component({
  selector: 'my-item',
  template: `
    <div>
      <label><input type="checkbox" [(ngModel)]="state"/> {{state}}</label>
    </div>
  `
})
export class MyItemComponent {
  state: boolean = false;
}

@Component({
  selector: 'my-app',
  template: `
    <div style="border: 1px solid red;">
      <label><input type="checkbox" [(ngModel)]="state"/> {{state}}</label>
    </div>
    <div *ngFor="#item of items">
      <my-item></my-item>
    </div>
  `,
  directives: [MyItemComponent]
})
export class App {
  state: boolean = true;
  items: Item[] = [];

  constructor() {
    this.items.push(new Item("hello"));
    this.items.push(new Item("test"));
  }
}
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
samsamm777
  • 1,648
  • 2
  • 14
  • 14

2 Answers2

6

Update

@Component({
  selector: 'my-item',
  inputs: ['state']; // added
  template: `
    <div>
      <label><input type="checkbox" [(ngModel)]="state"/> {{state}}</label>
    </div>
  `
})
export class MyItemComponent {
  state: boolean = false;
}

and then use it like

<my-item [state]="state"></my-item>

original

Angular change detection doesn't detect changes in arrays.
This should make it work:

  constructor() {
    this.items.push(new Item("hello"));
    this.items.push(new Item("test"));
    this.items = this.items.slice();
  }

This way a new array (a copy) is assigned to this.items and therefore Angular will recognize it as change.

In MyItem you need an input

@Component({
  selector: 'my-item',
  inputs: ['items']; // added
  template: `
    <div>
      <label><input type="checkbox" [(ngModel)]="state"/> {{state}}</label>
    </div>
  `
})
export class MyItemComponent {
  state: boolean = false;
  items: Item[]; // added
}

then you build the connection with

<my-item [items]="items"></my-item>

To get code called in MyItemComponent when items change implement ngOnChanges() See also https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html

export class MyItemComponent {
  state: boolean = false;
  items: Item[]; // added
  ngOnChanges(changes: {[propName: string]: SimpleChange}) {
    console.log('ngOnChanges - myProp = ' + changes['items'].currentValue);
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
3

What @Günter said, it's completely true!

That said, I see some mistakes into your plunkr:

@Component({
  selector: 'my-item',
  template: `
    <div>Hello</div>
  `
});  // <------- Remove the ;
export class MyItemComponent {

}

And you missed the component in the directives property:

@Component({
  selector: 'my-app',
  template: `
    <div>
      <label><input type="checkbox" [(ngModel)]="state"/> {{state}}</label>
    </div>
    <div *ngFor="#item of items">
      <my-item></my-item>
    </div>
  `,
  directives: [] // <-------- Add the MyItemComponent component
})
export class App {
  (...)
}

Edit

You can leverage the @ViewChildren decorator to reference children directly.

@Component({
  selector: 'my-app',
  template: `
    (...)
  `,
  directives: [MyItemComponent]
})
export class App {
  (...)
  @ViewChildren(MyItemComponent)
  children:MyItemComponent[];
  (...)
}

Then you can add a control to your checkbox to detect changes and update state of child components accordingly:

@Component({
  selector: 'my-app',
  template: `
    <div style="border: 1px solid red;">
      <label><input type="checkbox" [(ngModel)]="state"  
            [ngFormControl]="stateCtrl"/> {{state}}</label>
    </div>
    <div *ngFor="#item of items">
      <my-item></my-item>
    </div>
  `,
  directives: [MyItemComponent]
})
export class App {
  (...)
  constructor() {
    this.items.push(new Item("hello"));
    this.items.push(new Item("test"));

    this.stateCtrl = new Control();
    this.stateCtrl.valueChanges.subscribe(
      data => {
        this.children._results.forEach(child => {
          child.state = data;
        });
      });
  }
}

I updated your plunkr with this approach: https://plnkr.co/edit/nAA2VxZmWy0d4lljvPpU?p=preview.

See this link for more details: Why is that i can't inject the child component values into parent component?

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • I think I forgot to save my final changes to the Plunkr before pasting the link. Please see updated now. Im not sure the solutions are what im trying to achieve. When i tick/untick the checkbox inside the red, i want both/all children to update their state also. – samsamm777 Feb 08 '16 at 16:11
  • Oh! I forked and updated your plunkr with an approach based on the `@ViewChildren` decorator and form control. I updated my answer with its description... – Thierry Templier Feb 08 '16 at 16:26
  • Interesting approach, but it requires that the App component's logic stays in sync with the child component's template -- i.e., the App logic needs to know that the child component's template is using the `state` property. This is very tight coupling, hence rather fragile. It is better to use input properties, which act as a public API for the child component. Another way to reduce coupling (with your approach) is to add a method to the child component which sets the internal `state` property. Then the App component can call that public API method, rather than directly modify `state`. – Mark Rajcok Feb 09 '16 at 21:42