DEV Community

Cover image for Angular Tutorial: Signal Queries with the viewChild() and contentChild() Functions
Brian Treese
Brian Treese

Posted on • Originally published at briantree.se

Angular Tutorial: Signal Queries with the viewChild() and contentChild() Functions

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.

Example of a demo application before swithing to viewChild() and contentCHild() signal queries

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>;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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.

Slide explaining signal queries in Angular

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');
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

After:

private searchField = viewChild.required<ElementRef<HTMLInputElement>>('searchField');
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

After:

this.searchField().nativeElement.focus();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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.


Found This Helpful?

If you found this article helpful and want to show some love, you can always buy me a coffee!

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Brian Treese,
Your tips are very useful.
Thanks for sharing.