Angular Signal Queries with the viewChild() and contentChild() Functions
Brian Treese
Posted on April 21, 2024
If you work in Angular and haven’t heard yet, many things are in the process of switching away from decorators over to signals. I’ve already shown you how component or directive inputs have changed over from the @Input
decorator to signals with the input()
function. In this post we’ll take a look at how we can convert the @ViewChild
and @ContentChild
decorators over to signals with the viewChild()
and contentChild()
functions. We’ll take an example application that I previously created for a demo about the @ViewChild
and @ContentChild
decorators, and we’ll switch them over to the new signal functions producing the same end result. Ok, let’s get to it!
The Demo Application
So, here’s the demo application that I was talking about. It lists out a searchable list of NBA players.
We have created three examples:
- One where we used the
@ViewChild
decorator and a template reference variable to select the HTML input element for our search field in order to focus it on initialization. - Then, we have another example also using the
@ViewChild
decorator along with a component to focus the search form, on initialization, but when it’s nested within another component. - And, for the final example we used the
@ContentChild
decorator to focus the search field when it’s been included within projected content.
In this post, we’ll convert each of these over to signals using the viewChild()
and contentChild()
functions. Ok, let’s start with the @ViewChild
and template reference variable DOM element example.
Using the viewChild() Function to Query for an HTML Element Within a Component View
Ok, here in this example we can see, in our search component, we have the @ViewChild
decorator that queries for a template reference variable named “searchField”.
search.component.ts
@ViewChild('searchField', { static: true }) searchField!: ElementRef<HTMLInputElement>;
And if we switch over to the template, we can see the variable on our search input here.
search.component.html
<input type="search" id="search" #searchField>
Ok, let’s switch back to the code. A couple more things to note, we’ve also made this decorator static
because we know it will always exist. When we do this, it becomes available earlier in Angular’s component lifecycle events, so we’re able to set focus in the ngOnInit()
hook here.
search.component.ts
ngOnInit() {
this.searchField.nativeElement.focus();
}
If it wasn’t static, we’d need to add it in the ngAfterViewInit()
lifecycle event instead because it wouldn’t be available until the view is ready. Ok, now that we understand what’s going on here, let’s switch to the viewChild()
function.
What are Signal Queries?
This function is part of a set of what are known as “signal queries” in Angular. These queries are used to find child components, directives, or DOM elements.
The View:
If we’re searching for something directly within the component template, then we’re using the view.
The Content:
To contrast, if we’re searching for something that will be inserted within a content slot, we’re using content.
In this case, since we’re dealing with the component view, this function is considered a “view query”. Anything dealing with projected content would be considered a “content query”. We’ll see an example of this later on in the post.
Using the viewChild() Function
Ok, to use the viewChild()
function, we’ll create a private variable that we’ll name “searchField”. Since we’re going to replace our existing “searchField” property it’ll be named the same for now. We’ll remove the old one once we’re done. Then, we add the viewChild()
function and we’ll need to type it to an ElementRef
, HTMLInputElement
.
import { ..., viewChild } from '@angular/core';
@Component({
selector: 'app-search',
...
})
export class SearchComponent implements OnInit {
private searchField = viewChild<ElementRef<HTMLInputElement>>('searchField');
}
Ok, now we can remove the old variable set with the decorator and we can remove the decorator import too. Then, all we need to do is, since this property is now a signal, we need to add parenthesis where we’re using it.
ngOnInit() {
this.searchField()?.nativeElement.focus();
}
And that’s it. If we were to save, we should see that our search field is properly focused on initialization just like it was before the change.
Using an effect() Instead of ngOnInit()/ngAfterViewInit() With viewChild()
Now, since this property is now a signal, we can switch this over to use the effect()
function instead of the ngOnInit()
lifecycle event. This will make it available as soon as the viewChild()
signal is set.
To do this, let’s add a constructor()
. Within the constructor, let’s add the effect()
function. Within the effect()
callback, we can move our focus call.
import { ..., effect } from '@angular/core';
@Component({
selector: 'app-search',
...
})
export class SearchComponent implements OnInit {
constructor() {
effect(() => {
this.searchField()?.nativeElement.focus();
});
}
}
Now we can remove our ngOnInit()
function, interface, and import too. Then, we should be able to save and see this still working correctly. Pretty cool right?
Making a viewChild() Query Required
One more thing we can do here is, since we know that this field will always exist, we can make it required just like we can with the input()
function. We just add, required
to our function.
Before:
private searchField = viewChild<ElementRef<HTMLInputElement>>('searchField');
After:
private searchField = viewChild.required<ElementRef<HTMLInputElement>>('searchField');
Now, Typescript will infer that this variable will always exist, meaning we no longer need the question mark when we access it either.
Before:
this.searchField()?.nativeElement.focus();
After:
this.searchField().nativeElement.focus();
Ok, that’s it. If we were to save again, everything should work as expected.
Now, this would mean that we’d get an error if this were to become conditional or something in the future. So you'll just need to keep that in mind.
So that’s the viewChild()
function querying for an HTML element, now let’s query for a component within the template instead.
See the final result below:
Using the viewChild() Function to Query for a Component/Directive Within a Component View
Here, in this example, we have moved the search form to its own component. Then, this search form component gets included within the parent search component.
search.component.html
<app-search-form></app-search-form>
When we switch back over to the code for the search form component, we can see that we have our “searchField” variable available as a public property so that it can be accessed externally by other components or directives.
search-form.component.ts
@Component({
selector: 'app-search-form',
...
})
export class SearchFormComponent {
searchField = viewChild.required<ElementRef<HTMLInputElement>>('searchField');
}
Now, let’s take a look at the code for the search component. We have an old @ViewChild
decorator used to access the SearchFormComponent
within the view. It’s also static
like the previous example and it’s accessed in the ngOnInit()
lifecycle hook to set focus of the text field within that component.
search.component.ts
@Component({
selector: 'app-search',
...
})
export class SearchComponent implements OnInit {
@ViewChild(SearchFormComponent, { static: true }) searchForm!: SearchFormComponent;
ngOnInit() {
this.searchForm.searchField()?.nativeElement.focus();
}
}
This example will be very similar to the last one accept we’ll be using a component for our query instead of an HTML element. So, let’s add a new private variable named “searchForm”. Then we’ll use the viewChild()
function again. We’ll make it required, then we’ll pass it the SearchFormComponent
class. And that’s it.
private searchForm = viewChild.required(SearchFormComponent);
Now we can remove the old property, set with the decorator and we can remove the import for the @ViewChild
decorator.
Next, let's add a constructor()
, effect()
function, and move the focus call into the effect()
callback.
constructor() {
effect(() => {
this.searchForm().searchField()?.nativeElement.focus();
});
}
After that, we can now remove the ngOnInit()
function, the interface, and the import too. And that should be it. If we were to save, we should see everything still working properly.
So now we know how to query for both DOM elements and components or directives within the view. Up next, let’s look at how we’d use the contentChild()
function to query for a component within projected content.
See the final result below:
Using the contentChild() Function to Query for a Component/Directive Within Projected Content
In this example, we have our same search form component, but the parent search component has changed a little. If we look at the template, we can see that it has a content slot.
search.component.html
<app-search-form></app-search-form>
Let’s take a look at the app component template where this search component is used. Here, we can see that the search form component is included in the slot for the search component now.
app.component.html
<app-search>
<app-search-form></app-search-form>
</app-search>
...
So it’s projected content within the search component. Now to access this search form component from the parent search component we’ll be dealing with a content query.
Now let’s look at the code for the search component. Here we can see that we’re using the old @ContentChild
decorator to create a static “searchForm” property querying for the SearchFormComponent.
So, this is exactly the same as the last example accept we’ll use the contentChild()
function in this case.
Let’s create a private field named “searchForm”. Then we’ll use the contentChild()
function. We can make it required here too since we’ll always want to include it, and then we just pass the SearchFormComponent
class.
search.component.ts
private searchForm = contentChild.required(SearchFormComponent);
Now we can remove the old property set with the @ContentChild
decorator and we can remove the decorator import too.
Next, we can add a constructor. Then we can add the effect()
function. Then we can move the focus call into the effect()
.
constructor() {
effect(() => {
this.searchForm().searchField().nativeElement.focus();
});
}
Ok, all that’s left now is to remove the ngOnInit()
function, interface, and import. Now if we were save again, everything should be working correctly.
See the final result below:
Conclusion
So, there you have it. Now you know how use the new viewChild()
and contentChild()
signal query functions. They’re not too much different than the old decorators but you’ll probably want to start using them moving forward to modernize your Angular applications and now you’ll know how.
Posted on April 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.