Книга: Angular 2 Cookbook
Назад: Building a generalized Publish-Subscribe service to replace $broadcast, $emit, and $on
Дальше: Building a fully featured AutoComplete with Observables

Using QueryLists and Observables to follow changes in ViewChildren

One very useful piece of behavior in components is the ability to track changes to the collections of children in the view. In many ways, this is quite a nebulous subject, as the number of ways in which view collections can be altered is numerous and subtle. Thankfully, Angular 2 provides a solid foundation for tracking these changes.

Note

The code, links, and a live example of this are available at .

Getting ready

Suppose you begin with the following skeleton application:

[app/inner.component.ts]      import {Component, Input} from '@angular/core';      @Component({     selector: 'inner',     template: `<p>{{val}}`   })   export class InnerComponent {     @Input() val:number;   }   [app/outer.component.ts]      import {Component} from '@angular/core';      @Component({     selector: 'outer',     template: `       <button (click)="add()">Moar</button>       <button (click)="remove()">Less</button>       <button (click)="shuffle()">Shuffle</button>       <inner *ngFor="let i of list"               val="{{i}}">       </inner>     `   })   export class OuterComponent {     list:Array<number> = [];        add():void {       this.list.push(this.list.length)     }        remove():void {       this.list.pop();     }        shuffle():void {       // simple assignment shuffle       this.list = this.list.sort(() => (4*Math.random()>2)?1:-1);     }   }   

As is, this is a very simple list manager that gives you the ability to add, remove, and shuffle a list interpolated as InnerComponent instances. You want the ability to track when this list undergoes changes and keep references to the component instances that correspond to the view collection.

How to do it...

Begin by using ViewChildren to collect the InnerComponent instances into a single QueryList:

[app/outer.component.ts]      import {Component, ViewChildren, QueryList} from '@angular/core';   import {InnerComponent} from './inner.component';      @Component({     selector: 'outer',     template: `       <button (click)="add()">Moar</button>       <button (click)="remove()">Less</button>       <button (click)="shuffle()">Shuffle</button>       <inner *ngFor="let i of list"                  val="{{i}}">       </inner>     `   })   export class OuterComponent {     @ViewChildren(InnerComponent) innerComponents:        QueryList<InnerComponent>;     list:Array<number> = [];        add():void {       this.list.push(this.list.length)     }        remove():void {       this.list.pop();     }        shuffle():void {       // simple assignment shuffle       this.list = this.list.sort(() => (4*Math.random()>2)?1:-1);     }   }   

Easy! Now, once the view of OuterComponent is initialized, you will be able to use this.innerComponents to reference QueryList.

Dealing with QueryLists

QueryLists are strange birds in Angular 2, but like many other facets of the framework, they are just a convention that you will have to learn. In this case, they are an immutable and iterable collection that exposes a handful of methods to inspect what they contain and when these contents are altered.

In this case, the two instance properties you care about are last and changes. last, as you might expect, will return the last instance of QueryList—in this case, an instance of InnerComponent if QueryList is not empty. changes will return an Observable that will emit QueryList whenever a change occurs inside it. In the case of a collection of InnerComponent instances, the addition, removal, and shuffling options will all be registered as changes.

Using these properties, you can very easily set up OuterComponent to keep track of what the value of the last InnerComponent instance is:

import {Component, ViewChildren, QueryList} from '@angular/core';   import {InnerComponent} from './inner.component';      @Component({     selector: 'app-outer',     template: `       <button (click)="add()">Moar</button>       <button (click)="remove()">Less</button>       <button (click)="shuffle()">Shuffle</button>       <app-inner *ngFor="let i of list"                  val="{{i}}">       </app-inner>       <p>Value of last: {{lastVal}}</p>     `    })   export class OuterComponent {     @ViewChildren(InnerComponent) innerComponents:        QueryList<InnerComponent>;     list: Array<number> = [];     lastVal: number;        constructor() {}        add() {       this.list.push(this.list.length)     }        remove() {       this.list.pop();     }        shuffle() {       this.list = this.list.sort(() => (4*Math.random()>2)?1:-1);     }        ngAfterViewInit() {       this.innerComponents.changes         .subscribe(e => this.lastVal = (e.last || {}).val);     }   }     

With all of this, you should be able to find that lastVal will stay up to date with any changes you would trigger in the InnerComponent collection.

Correcting the expression changed error

If you run the application as is, you will notice that an error is thrown after you click on the Moar button the first time:

Expression has changed after it was checked   

This is an error you will most likely see frequently in Angular 2. The meaning is simple: since you are, by default, operating in development mode, Angular will check twice to see that any bound values do not change after all of the change detection logic has been resolved. In the case of this recipe, the emission by QueryList modifies lastVal, which Angular does not expect. Thus, you'll need to explicitly inform the framework that the value is expected to change again. This can be accomplished by injecting ChangeDetectorRef, which allows you to trigger a change detection cycle once the value is changed:

import {Component, ViewChildren, QueryList, ngAfterViewInit,      ChangeDetectorRef} from '@angular/core';   import {InnerComponent} from './inner.component';      @Component({     selector: 'outer',     template: `       <button (click)="add()">Moar</button>       <button (click)="remove()">Less</button>       <button (click)="shuffle()">Shuffle</button>       <inner *ngFor="let i of list"              val="{{i}}">       </inner>       <p>Value of last: {{lastVal}}</p>     `   })   export class OuterComponent implements AfterViewInit {     @ViewChildren(InnerComponent) innerComponents:        QueryList<InnerComponent>;     list:Array<number> = [];     lastVal:number;        constructor(private changeDetectorRef_:ChangeDetectorRef) {}        add():void {       this.list.push(this.list.length)     }        remove():void {       this.list.pop();     }        shuffle():void {       // simple assignment shuffle       this.list = this.list.sort(() => (4*Math.random()>2)?1:-1);     }          ngAfterViewInit() {       this.innerComponents.changes         .subscribe(innerComponents => {           this.lastVal = (innerComponents.last || {}).val;           this.changeDetectorRef_.detectChanges();         });     }   }   

At this point, everything should work correctly with no errors.

How it works...

Once the OuterComponent view is initialized, you will be able to interact with QueryList that is obtained using ViewChildren. Each time the collection that QueryList wraps is modified, the Observable exposed by its changes property will emit QueryList, signaling that something has changed.

Hate the player, not the game

Importantly, Observable<QueryList> does not track changes in the array of numbers. It tracks the generated collection of InnerComponents. The ngFor structural directive is responsible for generating the list of InnerComponent instances in the view. It is this collection that QueryList is concerned with, not the original array.

This is a good thing! ViewChildren should only be concerned with the components as they have been rendered inside the view, not the data that caused them to be rendered in such a fashion.

One important consideration of this is that upon each emission, it is entirely possible that QueryList will be empty. As shown above, since the Observer of the QueryList.changes Observable tries to reference a property of last, it is necessary to have a fallback object literal in the event that last returns undefined.

See also

  • Basic Utilization of Observables with HTTP demonstrates the basics of how to use an observable interface
  • Building a fully featured AutoComplete with Observables gives you a broad tour of some of the utilities offered to you as part of the RxJS library
Назад: Building a generalized Publish-Subscribe service to replace $broadcast, $emit, and $on
Дальше: Building a fully featured AutoComplete with Observables

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already