From b975ff1bc220371843a7abb655a77485237ec45e Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 12 Feb 2024 15:57:59 +0100 Subject: [PATCH] docs: add guide for signal queries (#54395) Adding new guide for the signal-based queries. PR Close #54395 --- .pullapprove.yml | 1 + aio/content/guide/signal-queries.md | 194 ++++++++++++++++++++++++++++ aio/content/navigation.json | 5 + 3 files changed, 200 insertions(+) create mode 100644 aio/content/guide/signal-queries.md diff --git a/.pullapprove.yml b/.pullapprove.yml index d51fb3f7add..240a7d3960a 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -322,6 +322,7 @@ groups: 'aio/content/guide/providers.md', 'aio/content/examples/providers/**/{*,.*}', 'aio/content/images/guide/providers/**/{*,.*}', + 'aio/content/guide/signal-queries.md', 'aio/content/guide/singleton-services.md', 'aio/content/guide/signal-inputs.md', 'aio/content/guide/sharing-ngmodules.md', diff --git a/aio/content/guide/signal-queries.md b/aio/content/guide/signal-queries.md new file mode 100644 index 00000000000..4765ac70a00 --- /dev/null +++ b/aio/content/guide/signal-queries.md @@ -0,0 +1,194 @@ +# Signal queries + +A component or directive can define queries that find child elements and read values from their injectors. + +Developers most commonly use queries to retrieve references to components, directives, DOM elements, and more. + +There are two categories of query: view queries and content queries. + +Signal queries supply query results as a reactive signal primitive. You can use query results in `computed` and `effect`, composing these results with other signals. + +
+Signal queries are in [developer preview](/guide/releases#developer-preview). +APIs may change based on feedback without going through Angular's deprecation cycle. +
+ +If you're already familiar with Angular queries, you can jump straight to [Comparing signal-based queries to decorator-based queries](#comparing-signal-based-queries-to-decorator-based-queries) + +## View queries + +View queries retrieve results from the elements in the component's own template (view). + +### `viewChild` + +You can declare a query targeting a single result with the `viewChild` function. + +```ts +@Component({ + template: ` +
+ + ` +}) +export class TestComponent { + // query for a single result by a string predicate + divEl = viewChild('el'); // Signal + // query for a single result by a type predicate + cmp = viewChild(MyComponent); // Signal +} +``` + +### `viewChildren` + +You can also query for multiple results with the `viewChildren` function. + +```ts + @Component({ + template: ` +
+ @if (show) { +
+ } + ` +}) +export class TestComponent { + show = true; + + // query for multiple results + divEls = viewChildren('el'); // Signal> +} + ``` + +### View query options + +The `viewChild` and the `viewChildren` query declaration functions have a similar signature accepting two arguments: + +* a **locator** to specify the query target - it can be either a `string` or any injectable token +* a set of **options** to adjust behavior of a given query. + +Signal-based view queries accept only one option: `read`. The `read` option indicates the type of result to inject from the matched nodes and return in the final results. + +```ts +@Component({ + template: `` +}) +export class TestComponent { + // query for a single result with options + cmp = viewChild(MyComponent, {read: ElementRef}); // Signal +} +``` + +## Content queries + +Content queries retrieve results from the elements in the component's content — the elements nested inside the component tag in the template where it's used. + +### `contentChild` + +You can query for a single result with the `contentChild` function. + +```ts + @Component({...}) + export class TestComponent { + // query by a string predicate + headerEl = contentChild('h'); // Signal + + // query by a type predicate + header = contentChild(MyHeader); // Signal + } + ``` + + ### `contentChildren` + +You can also query for multiple results with the `contentChildren` function. + +```ts + @Component({...}) + export class TestComponent { + // query for multiple results + divEls = contentChildren('h'); // Signal> + } + ``` + +### Content query options + +The `contentChild` and the `contentChildren` query declaration functions have a similar signature accepting two arguments: + +* a **locator** to specify the query target - it can be either a `string` or any injectable token +* a set of **options** to adjust behavior of a given query. + +Content queries accept the following options: + +* `descendants` By default, content queries find only direct children of the component and do not traverse into descendants. If this option is changed to `true`, query results will include all descendants of the element. Even when `true`, however, queries _never_ descend into components. +* `read` indicates the type of result to retrieve from the matched nodes and return in the final results. + +### Required child queries + +If a child query (`viewChild` or `contentChild`) does not find a result, its value is `undefined`. This may occur if the target element is hidden by a control flow statement like`@if` or `@for`. + +Because of this, the child queries return a signal that potentially have the `undefined` value. Most of the time, and especially for the view child queries, developers author their code such that: +* there is at least one matching result; +* results are accessed when the template was processed and query results are available. + +For such cases, you can mark child queries as `required` to enforce presence of at least one matching result. This eliminates `undefined` from the result type signature. If a `required` query does not find any results, Angular throws an error. + +```ts +@Component({ + selector: 'app-root', + standalone: true, + template: ` +
+ `, +}) +export class App { + existingEl = viewChild.required('requiredEl'); // required and existing result + missingEl = viewChild.required('notInATemplate'); // required but NOT existing result + + ngAfterViewInit() { + console.log(this.existingEl()); // OK :-) + console.log(this.missingEl()); // Runtime error: result marked as required by not available! + } +} +``` + +## Results availability timing + +A signal query authoring functions will be executed as part of the directive instance construction. This happens before we could create a query instance and execute the template’s creation mode to collect any matches. As a consequence, there is a period of time where the signal instance was created (and can be read) but no query results could have been collected. By default Angular will return `undefined` (for child queries) or an empty array (for children queries) before results are available. Required queries will throw if accessed at this point. + +Angular computes signal-based query results lazily, on demand. This means that query results are not collected unless there is a code path that reads the signal. + +Query results can change over time due to the view manipulation - either through the Angular's control flow (`@if`, `@for` etc.) or by the direct calls to the `ViewContainerRef` API. When you read the value from the query result signal, you can receive different values over time. + +Note: to avoid returning incomplete query results while a template is rendered, Angular delays query resolution until it finishes rendering a given template. + +## Query declarations functions and the associated rules + +The `viewChild`, `contentChild`, `viewChildren` and `contentChildren` functions are special function recognized by the Angular compiler. You can use those functions to declare queries by initializing a component or a directive property. You can never call these functions outside of component and directive property initializers. + +```ts +@Component({ + selector: 'app-root', + standalone: true, + template: ` +
+ `, +}) +export class App { + el = viewChild('el'); // all good! + + constructor() { + const myConst = viewChild('el'); // NOT SUPPORTED + } +} +``` + +## Comparing signal-based queries to decorator-based queries + +Signal queries are an alternative approach to the queries declared using the `@ContentChild`, `@ContentChildren`, `@ViewChild` or `@ViewChildren` decorators. The new approach exposes query results as signals which means that query results can be composed with other signals (using `computed` or `effect`) and drive change detection. Additionally, the signal-based query system offers other benefits: + +* **More predictable timing.** You can access query results as soon as they're available. +* **Simpler API surface.** All queries return a signal, and queries with more than one result let you work with a standard array. +* **Improved type safety.** Fewer query use cases include `undefined` in the possible results. +* **More accurate type inference.** TypeScript can infer more accurate types when you use a type predicate or when you specify an explicit `read` option. +* **Lazier updates.** - Angular updates signal-based query results lazily; the framework does no work unless your code explicitly reads the query results. + +The underlying query mechanism doesn't change much - conceptually Angular still creates singular "child" or plural "children" queries that target elements in a template (view) or content. The difference is in type of results and the exact timing of the results availability. The authoring format for declaring signal-based queries changed as well: the `viewChild`, `viewChildren`, `contentChild` and `contentChildren` functions used as initializer of class members are automatically recognized by Angular. diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 87056b57c46..b40a76f5f19 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -792,6 +792,11 @@ "title": "Signal inputs", "tooltip": "Signal inputs to optimize change detection and developer experience." }, + { + "url": "guide/signal-queries", + "title": "Signal queries", + "tooltip": "Signal queries exposing results as a reactive signal primitive." + }, { "url": "guide/rxjs-interop", "title": "RxJS Interop for Signals",