The Angular @ViewChild decorator

Cédric Rémond - Jun 19 '19 - - Dev Community

With Angular 8, the static parameter of the @ViewChild decorator became temporary mandatory.
In the previous versions, Angular automatically decided if the query had to be static or dynamic and, as I wasn't familiar with this parameter, I thought it was a good time to dig into it and to write my first blog post ever! 😄

In this post I'll briefly introduce what's a decorator and how it is used and then we'll dig into the @ViewChild decorator and explain the role of its parameters.

Decorators and the decorator pattern

The word decorator can refer to two different things when talking about programming languages: the decorator pattern, and actual decorators. Let's demystify these concepts!

The decorator pattern

The decorator pattern is an OOP design pattern allowing to add behavior to a class or a class member dynamically. It means that, for example, we can change the behavior of a class at the instantiation of an object, without changing the behavior of further instantiations. I do not want to dig too deep in the explanation of this design pattern here.

Decorators

A decorator as we will talk about in this article is a specific implementation of the decorator pattern in a programming language. As this article is about Angular and TypeScript, we will use the word decorator to designate the specific implementation of this design pattern in TypeScript.

Decorators are an experimental TypeScript feature, so breaking changes can be introduced anytime. However, the Angular syntax relies on decorators heavily.

Basically, a decorator in TypeScript is a function that can be attached to a class or a class member (an attribute or a method) using an annotation beginning by @. A decorator can take parameters.
For example, we could define a @isAdmin decorator used in a component like this:

user = this.userService.getUser();

@isAdmin(user)
deleteEntry(entryId: string) {
// Delete some entry.
}
Enter fullscreen mode Exit fullscreen mode

And this decorator can be used for example to restrict the access of the method to user who have the admin role.

The decorator declaration could be something like that:

import { jwtDecode } from 'jwt-decode';

function isAdmin(user: User) {
  return jwtDecode(user.token).isAdmin;
}
Enter fullscreen mode Exit fullscreen mode

Pretty cool, isn't it?
Decorators can help us structure our code by wrapping behavior in reusable functions.

If you are familiar with Angular, you probably noticed how we declare Angular components, modules, etc. For example an Angular component is a class annotated with the @Component decorator and this decorator take some parameters like its template URL and its change detection strategy.

Another decorator provided by Angular is @ViewChild.It is this one we'll focus is this article!

The Angular @ViewChild decorator

The @ViewChild decorator can be applied on a property and allow to configure a view query.

The selector

The first parameter of this decorator is the selector. Angular will use the selector to try matching an element in the template, and the property annotated with the decorator will reference the first matching element.

A selector can take several forms, so let's explore them and write some examples.

  • any class with the @Component or @Directive decorator
@Component({
  selector: 'user-card'
})
export class UserCard {
  @Input() firstName: string;
  @Input() lastName: string;
  @Input() age: number;
}

@Component({
  selector: 'myComp',
  template: `
    <user-card [firstName]="'Roger'" [lastName]="'Dupont'" [age]="53">
    </user-card>
  `
})
export class MyCompComponent {
  @ViewChild(UserCard, { static: false }) userCard: UserCard;
}
Enter fullscreen mode Exit fullscreen mode
@Directive({
  selector: 'myMenu'
})
export class MenuDirective {
  open() {}
  close() {}
}

@Component({
  selector: 'my-comp',
  template: `
    <div myMenu></div>
  `
})
export class MyCompComponent {
  @ViewChild(MyMenuDirective, { static: false }) menu: MyMenuDirective;
}
Enter fullscreen mode Exit fullscreen mode
  • a template reference variable as a string
@Component({
  selector: 'my-comp',
  template: `
    <div #someElement></div>
  `
})
export class MyCompComponent {
  @ViewChild('someElement', { static: false }) someElement: ElementRef;
}
Enter fullscreen mode Exit fullscreen mode
  • a TemplateRef
@Component({
  selector: 'my-comp',
  template: `
    <ng-template></ng-template>
  `
})
export class MyCompComponent {
  @ViewChild(TemplateRef, { static: false }) someTemplate: TemplateRef;
}
Enter fullscreen mode Exit fullscreen mode

The Angular documentation states there are two other selector possibilities:

  • any provider defined in the child component tree of the current component (e.g. @ViewChild(SomeService) someService: SomeService)
  • any provider defined through a string token (e.g. @ViewChild('someToken') someTokenVal: any)

However I have no clue how to apply these cases. If someone has the answer and want to give a hand, she or he would be very welcomed. 😉

The static parameter

Here we are, the parameter that became temporary mandatory! Let's see what its role is.

The static parameter, and I'm sure you guessed, is here to tell Angular if the query should be ran statically or dynamically. But what does this change in practice?
Basically, it changes when the view query will resolve.

Angular recommends retrieving view queries results in the ngAfterViewInit lifecycle hook to ensure that queries matches that are dependent on binding resolutions (like in *ngFor loops or *ngIf conditions) are ready and will thus be found by the query. To get this behavior, the static parameter must be set to false.

Let's see an example (open the StackBlitz console to see the logs):

Setting the static parameter to false cover most of our use cases. However, we may encounter situation where we need to access the view query result before the ngAfterVewInit hook is called. Setting static to true allow this behavior by allowing to access the view query results in the ngOnInit lifecycle hook, but it only works for queries taht can be resolved statically. The element we want to fetch with @ViewChild must so not be in a *ngFor loop or a *ngIf condition.

Let's see an example:

As said in the Angular documentation, static is only mandatory in version 8 to ease the change of default and avoid further errors. By making developers think about this parameter, they are prepared for the next default behavior of @ViewChild.

From version 9, the static parameter default value will be false. The previous behavior (the default value was automatically determined by Angular depending on how the view query result was used) could lead to some tricky bugs.

The read parameter

The read parameter is optional. This parameter allows to change the type of the view query result. In fact, each kind of selector has its default type:

  • any class with the @Component or @Directive decorator ➡️ the class
  • a template reference variable as a string ️️️➡️ ️️️ElementRef
  • a TemplateRef ➡️ TemplateRef

However, we may want to query using a template reference variable as a string and use the actual type of the targeted element. In the same fashion, we can use a class as a selector and want to access it though the ElementRef type.

A non-exhaustive list of examples:

@Component({
  selector: 'my-comp',
  template: `
    <user-card #userCard></user-card>
  `
})
export class MyCompComponent {
  // We set read to the UserCard type corresponding to a component class, so the view query result will be of type UserCard.
  @ViewChild('userCard', { read: UserCard, static: false }) userCard: UserCard;
}
Enter fullscreen mode Exit fullscreen mode

Using a component or directive class allows to access the properties of this class. For example, a UserCard component representing a card with user information could countain a method, and this method could thus be used programatically from the view query result. It would look like this.userCard.flip();.

@Component({
  selector: 'my-comp',
  template: `
    <user-card></user-card>
  `
})
export class MyCompComponent {
  // We set read to ElementRef so, even if the selector is a component class, the query view result will be of type ElementRef.
  @ViewChild(UserCard, { read: ElementRef, static: false })
  userCard: ElementRef;
}
Enter fullscreen mode Exit fullscreen mode

ElementRef is a wrapper around a native element, so it is useful to access things like HTML attributes, classes, etc.

@Component({
  selector: 'my-comp',
  template: `
    <div #myContainer></div>
  `
})
export class MyCompComponent {
  @ViewChild('myContainer', { read: ViewContainerRef, static: false })
  myList: ViewContainerRef;
}
Enter fullscreen mode Exit fullscreen mode

ViewContainerRef allows to get the element as container. This is the good choice when we need to manipulate the DOM (for example adding or removing nodes dynamically).

This parameter allows our queries to be very flexible as the returned type can be independent of the kind of selector we choose to use.

A quick view on @ViewChildren

There is another Angular decorator called @ViewChildren.

As we saw before, a @ViewChild query only return the first matching element. So what if we want to get the list of all matching elements? That's exactly what @ViewChildren is for.

It takes a selector and a read parameter like @ViewChild, but no static. The only available behavior is dynamic, so the query will only resolve in the ngAfterViewInit lifecycle hook.

@ViewChildren returns a QueryList object, which contains an EventEmitter object. The QueryList is dynamically updated, so if matching elements are added or deleted, the QueryList will emit a new event, so we can subscribe on it and react on value change.

First article in the wild

Yay, you reach the end of my first article ever, congratulations!

Any suggestions and remarks are welcomed 😄

Useful links and sources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .