DEV Community

Cover image for Angular Signal Queries: Simplifying DOM Querying
Davide Passafaro
Davide Passafaro

Posted on • Originally published at codemotion.com

Angular Signal Queries: Simplifying DOM Querying

Following Signal Inputs and Model Inputs, another Signal API has landed in the Angular ecosystem with the release of v17.2.0: Signal Queries.

Signal Queries offer an alternative approach to the decorator-based queries, namely @ViewChild, @ViewChildren, @ContentChild and @ContentChildren, supplying query results as a Signal.

With queries, you can retrieve references to components, directives, DOM elements, and more. Let’s see what has changed with Signal Queries.


View queries

View queries allow you to retrieve and interact directly with elements in a component’s own template, also known as view.

Both @ViewChild and @ViewChildren now have a Signal counterpart.

@ViewChild to viewChild(), @ViewChildren to viewChildren()

viewChild

Using the new viewChild() function you can retrieve a single element:

import { Component, ElementRef, Signal, viewChild } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: '<input #inputEl />',
})
export class MyComponent {
  inputElement: Signal<ElementRef | undefined> = viewChild('inputEl');
}
Enter fullscreen mode Exit fullscreen mode

This function accepts every parameter that @ViewChild supports and offers the same functionalities, providing you the query result as a Signal.

When a query does not find any result its value is undefined, this happens commonly using Control Flow statement like @if and @for.
Because of this, a viewChild() Signal value type is ElementRef | undefined.

If you are sure about the presence of at least one matching result, you can use the dedicated viewChild.required() function and get rid of undefined:

import { Component, ElementRef, Signal, viewChild } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: '<input #inputEl />',
})
export class MyComponent {
  inputElement: Signal<ElementRef> = viewChild.required('inputEl');
}
Enter fullscreen mode Exit fullscreen mode

But be careful, when a required query fails to find any results, Angular throws a dedicated error:

Angular Error NG0951

viewChildren

Using the new viewChildren() function you can retrieve multiple elements:

import { Component, ElementRef, Signal, viewChildren } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: `
    <input #inputEl />

    @if (showSecondInput) {
      <input #inputEl />
    }
  `,
})
export class MyComponent {
  showSecondInput = true;

  inputElementList: Signal<readonly ElementRef[]> = viewChildren('inputEl');
}
Enter fullscreen mode Exit fullscreen mode

Similarly to viewChild() API, the viewChildren() function accepts every parameter that @ViewChildren supports and offers the same functionalities, providing you the query result as a Signal.

When a viewChildren() query does not find any result, its value is an empty array, this guarantees the results array is always initialized.


Content queries

Content queries allow you to retrieve and interact directly with elements in a component’s content.

A component’s content is represented by the elements provided thought content projection, nesting elements inside the component tag in the template where it is used. For example:

<my-component>
  <span> Hello ;) </span>

  <input #inputEl />
</my-component>
Enter fullscreen mode Exit fullscreen mode

Note: apart for the target template where the queries are performed, content queries APIs and view queries APIs work identically.
Because of that the next section of the article totally relies on the previous one.

Similar to our previous discussion on view queries, both @ContentChild and @ContentChildren now have as well a Signal counterpart.

@ContentChild to contentChild(), @ContentChildren to contentChildren()

contentChild

Using the new contentChild() and contentChild.required() functions you can retrieve a single element:

import { Component, contentChild, ElementRef, Signal } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: `
    <div>
      <ng-content />
    </div>
  `,
})
export class MyComponent {
  inputElement: Signal<ElementRef | undefined> = contentChild('inputEl');

  inputElementReq: Signal<ElementRef> = contentChild.required('inputEl');
}
Enter fullscreen mode Exit fullscreen mode

Identically to the viewChild() function, when a required contentChild() function query fails to find any results, Angular throws a dedicated error:

Angular Error NG0951

contentChildren

Using the contentChildren() function you can retrieve multiple elements:

import { Component, contentChildren, ElementRef, Signal } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: `
    <div>
      <ng-content />
    </div>
  `,
})
export class MyComponent {
  inputElementList: Signal<readonly ElementRef[]> = contentChildren('inputEl');
}
Enter fullscreen mode Exit fullscreen mode

Identically to the viewChildren() function, when a contentChildren() query does not find any result, its value is an empty array.


Some rules you need to be careful

This new functions, viewChild(), viewChildren(), contentChild() and contentChildren(), only work if used to declare queries by initializing a component or a directive property.

Calling them outside of component and directive property initialization will produce no error, but the query will not find any result:

import { Component, ElementRef, Signal, viewChild } from '@angular/core';

@Component({
  selector: 'my-component',
  standalone: true,
  template: `
    <div #el></div>
  `,
})
export class MyComponent {
  el = viewChild('el'); // It works!

  constructor() {
    const myEl: Signal<undefined> = viewChild('el'); // No error
    console.log(myEl()); // undefined
  }
}
Enter fullscreen mode Exit fullscreen mode

Signal Queries vs Decorator Queries

Having the queries results as Signals means that we can compose them with other Signals, using computed() and effect() functions.

This is a significant advantage, as it enhances the flexibility and functionality of our codebase, leading the way to all the improvements in change detection that Signals brought to the framework.

In addition to that, Signal Queries offer other benefits:

  • More predictable timing: query results are accessible as soon as they’re available;

  • Simpler API surface: every query result is provided as a Signal, and queries with multiple results (viewChildren() and contentChildren()) return always a defined array;

  • Improved type safety: cases with undefined as possible result are fewer, thanks to required() functions and default array for multiple results;

  • More accurate type inference: TypeScript can infer more accurate types when a type predicate is used or when you explicit a read option;

  • Lazier updates: as for all Signals, Angular updates queries results lazily. This means that the read operation are performed only when your code explicitly reads the query results.


Thanks for reading so far 🙏

I’d like to have your feedback so please leave a comment, like or follow. 👏

Then, if you really liked it, share it among your community, tech bros and whoever you want. And don’t forget to follow me on LinkedIn. 👋😁

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Davide Passafaro,
Your tips are very useful.
Thanks for sharing.