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.
}
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;
}
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;
}
@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;
}
- a template reference variable as a string
@Component({
selector: 'my-comp',
template: `
<div #someElement></div>
`
})
export class MyCompComponent {
@ViewChild('someElement', { static: false }) someElement: ElementRef;
}
- a TemplateRef
@Component({
selector: 'my-comp',
template: `
<ng-template></ng-template>
`
})
export class MyCompComponent {
@ViewChild(TemplateRef, { static: false }) someTemplate: TemplateRef;
}
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;
}
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;
}
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;
}
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 😄