Angular QueryList changes

Once you master the basics of Angular and start to extend your knowledge of the framework, you can start to use more sophisticated types of inputs in your component instead of the regular @Input decorator. In this post we will go through all the ways we can leverage the QueryList object, understand the differences between the different children it binds to, and finally register to its changes including the initial value.

ng-content

This tag is used for content projection meaning it’s a placeholder to hold dynamic content. Once Angular parsed the dynamic content it will replace the ng-content tag with the parsed dynamic content. You can think about it as using the {{ }}(curly braces) in the template but the difference is the value will not be parsed from the state in the component but from the content of the user of your component(A fellow developer or you) placed inside your tag.

Let’s say we are creating a UI Card widget we call CardCompnent with the selector my-card that will be used as a general cardholder in our application. We want to project dynamic content into it so we can reuse the design but with a different body as needed. The card template will look something like this:

<div class="card ">
  <p class="card-body">
    <ng-content></ng-content> 
  </p>
</div>

Now whenever someone wishes to use this widget he will invoke it as the following:

<my-card>
 <h3> This is my dynamic - changing content</h3>
</my-card>

Notice the h3 tag – this is what will be projected and will replace the ng-content tag after the parse.

Now that we understand the difference between dynamic content and static content we can move on with the explanation.

@ViewChildren

Use to get the QueryList of elements or directives from the view “DOM”. Meaning nondynamic content that will be injected into an ng-content tag. Any time a child element is added, removed, or moved, the query list will be updated, and the changes observable in the query list will emit a new value. View queries are set before the ngAfterViewInit callback is called.

@ContentChildren

Used to get the `QueryList` elements or directives from the content DOM. Meaning elements that will be projected into the “ng-contnent” tag are not statically rendered. Any time a child element is added, removed, or moved, the query list will be updated, and the changes observable in the query list will emit a new value. Content queries are set before the `ngAfterContentInit` callback is called. Do not retrieve elements or directives that are in other components’ templates.

@ViewChildren vs @ContentChildren

What is the difference? Let us take a look at the following example:

templeate:
     <my-card>
           <my-card-body></my-card-body>
     </my-card>

The first descendent of the parent element is the my-card component that we added ourselves so to query child elements we will use the @ViewChildren since its static, as for the grand-child – my-card-body component, this element will be projected into the my-card once it is parsed using an ng-content tag so we should use the @ContentChildren since its the dynamic content of the my-card component.

When we look for natural elements in our template we will use @ViewChildren when we are querying for elements that will be used not directly by us as the my-card-body component then we go with @ContentChildren.

QueryList

An unmodifiable list of items that Angular keeps up to date when the state * of the application changes. Changes can be observed by subscribing to the changes `Observable`.

Take a look at the class definition – we can notice that this is an array-like object with some more extra properties

class QueryList<T> implements Iterable {
  constructor(_emitDistinctChangesOnly: boolean = false)
  dirty: true
  length: number
  first: T
  last: T
  changes: Observable<any>
  __@iterator: () => Iterator<T>
  get(index: number): T | undefined
  map<U>(fn: (item: T, index: number, array: T[]) => U): U[]
  filter(fn: (item: T, index: number, array: T[]) => boolean): T[]
  find(fn: (item: T, index: number, array: T[]) => boolean): T | undefined
  reduce<U>(fn: (prevValue: U, curValue: T, curIndex: number, array: T[]) => U, init: U): U
  forEach(fn: (item: T, index: number, array: T[]) => void): void
  some(fn: (value: T, index: number, array: T[]) => boolean): boolean
  toArray(): T[]
  toString(): string
  reset(resultsTree: (any[] | T)[], identityAccessor?: (value: T) => unknown): void
  notifyOnChanges(): void
  setDirty()
  destroy(): void
}

Next, we choose to use one of the above decorators and will end up with a QueryList object that we can track for changes

@ContentChildren() myChildren: QueryList<SomeComponent>;

We can keep track of changes using the property

changes: Observable<any>;

Since it’s just an observable we can just subscribe to is

myChildren.changes.subscribe(change=> doSomething());

But….. don’t forget that these changes will be effected after the ngAfterViewInit and ngAfterContentInit depending on if its @ViewChildren or @ContentChildren respectability. meaning you will miss the first change that will occur with the initial value. This can be solved using the startWith operator

myChildren.changes.pipe(startWith(0)).subscribe(change=> doSomething());

Using the Rxjs startwith observable

Since A BehaviorSubject can also start with an initial value and changes will occur after the initial value is already set we are going to miss the initial value. – startWith(0) also solves this issue by omitting the initial value. So now we will pick up changes in the QueryList object but we will also receive the inial value that it was inited with!

Example of ongoing changes:

templeate:
     <my-card>
           <my-card-body *ngIf="shouldDisplay"></my-card-body>
     </my-card>

Notice the inner my-card-body has a ngIf which means it can change on the fly so the changes Observable will emit.

Learning Angular – check out the rest of our posts here.

Resources:

Angular – Understanding how to use QueryList properly.

Also published on Medium.

Yoni Amishav


Tech lead, blogger, node js Angular and more ...


Post navigation


Leave a Reply

Free Email Updates
Get the latest content first.
We respect your privacy.
%d