Introduction
In this blog post, I would like to show a new feature in Angular 17.2 that is called signal queries. This new feature allows Angular engineers to use viewChild, viewChildren, contentChild, and contentChildren to obtain the value as a signal.
In my demos, I would like to apply these signal queries on a template-driven form and dynamic template respectively. Readers will find out the three examples below:
- Apply viewChild on template-driven form
- Apply viewChildren to obtain a signal of button array. When I click a button, the background color changes and a template is shown in ngTemplateOutlet
- Repeat the second example, but use contentChild and contentChildren this time
Update the version of the angular dependencies
"@angular/animations": "^17.2.0-rc.1",
"@angular/common": "^17.2.0-rc.1",
"@angular/compiler": "^17.2.0-rc.1",
"@angular/core": "^17.2.0-rc.1",
"@angular/forms": "^17.2.0-rc.1",
"@angular/platform-browser": "^17.2.0-rc.1",
"@angular/router": "^17.2.0-rc.1",
In package.json, update the version of the angular dependencies to 17.2.0-rc.1 to access the new signal queries feature.
Demo 1: use viewChild to access a template-driven form
type FormModel = {
name: string;
address: {
address1: string;
address2: string;
postalCode: string;
}
};
Define the mode of the template-driven form. The form has a name input and a form group that includes address1, address2, and postal code.
// app-simple-form.component.ts
<form #f="ngForm">
<div>
<label for="name">
<span>Name: </span>
<input id="name" name="name" type="text" required minlength="3" ngModel size="30">
</label>
</div>
<div ngModelGroup="address">
<div>
<label for="address1">
<span>Address 1: </span>
<input id="address1" name="address1" type="text" required minlength="3" ngModel size="50">
</label>
</div>
<div>
<label for="address2">
<span>Address 2: </span>
<input id="address2" name="address2" type="text" required minlength="3" ngModel size="50">
</label>
</div>
<div>
<label for="postalcode">
<span>Postal Code: </span>
<input id="postalCode" name="postalCode" type="text" required ngModel>
</label>
</div>
</div>
<button type="submit" [disabled]="!vm.isFormValid">Submit</button>
</form>
@if (vm.isSubmitted) {
<p>Data submitted: </p>
<pre>
{{ vm.formValues | json }}
</pre>
}
The template-driven form has a template reference f that viewChild uses to obtain a reference to NgForm. When user clicks the submit button, the data is printed inside the
block.Manipulate form state with viewChild
// app-simple-form.component.ts form = viewChild.required('f', { read: NgForm });The demo uses the
viewChildto query a NgForm with a template reference namedf. The result isNgFormthat is assigned toformmember.formValues = signal<FormModel>({ name: '', address: { address1: '', address2: '', postalCode: '', } }); isFormValid = signal(false); isFormSubmitted = signal(false); viewModel = computed(() => { return { formValues: this.formValues(), isFormValid: this.isFormValid(), isSubmitted: this.isFormSubmitted(), } }); get vm() { return this.viewModel(); }
formValuesis a Signal that stores the form data when it changes.isFormValidis a Signal that stores whether or not a form is valid.isFormSubmittedis a boolean Signal that sets to true when a user clicks the submit button.
viewModelis a computed signal that represents the view model of the component. Finally, I define avmgetter to return the value of theviewModelsignal.This is my personal preference and you don't have to follow it.
// main.ts constructor() { effect((onCleanup) => { const formValueChanges$ = this.form().form.valueChanges.pipe( debounceTime(0) ); const sub = formValueChanges$.subscribe((v: FormModel) => { this.formValues.set(v); this.isFormValid.set(this.form().valid || false); }); this.form().ngSubmit.subscribe(() => this.isFormSubmitted.set(true) ) onCleanup(() => sub.unsubscribe()); }); } }When the form values change, effect is run to update
formValues,isFormValidandisFormSubmittedsignals respectively. Then,viewModelis recomputed and it updates thevmgetter. The button is disabled whenvm.isFormValidis false. Similarly, whenvm.isFormSubmittedis true, the form values appear within theblock.Demo 2: use viewChild and viewChildren to select a dynamic template
// app-viewchild.component.ts @Component({ selector: 'app-viewchild', standalone: true, imports: [NgClass, NgTemplateOutlet], template: ` <div class="container"> <p>viewchild and viewchild demo</p> <div> <p>Select a template</p> <button #el (click)="lastClickedBtn.set(1)" data-id="1" [ngClass]="vm.btnClasses[0]">1</button> <button #el (click)="lastClickedBtn.set(2)" data-id="2" [ngClass]="vm.btnClasses[1]">2</button> <button #el (click)="lastClickedBtn.set(3)" data-id="3" [ngClass]="vm.btnClasses[2]">3</button> <button #el (click)="lastClickedBtn.set(4)" data-id="4" [ngClass]="vm.btnClasses[3]">4</button> </div> <ng-container *ngTemplateOutlet="vm.template; context: { $implicit: lastClickedBtn() }" /> </div> <ng-template #t let-id> <p>Simple Template {{ id }}</p> </ng-template> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppViewChild { lastClickedBtn = signal<number>(1); btnGroups = viewChildren<ElementRef<HTMLButtonElement>>('el'); t = viewChild.required('t', { read: TemplateRef }); btnClasses = computed(() => { return this.btnGroups().map((b) => { const e = b.nativeElement; const id = +(e.dataset['id'] || '1'); return id === this.lastClickedBtn() ? 'last-clicked' : ''; }); }); viewModel = computed(() => ({ template: this.t(), lastClicked: this.lastClickedBtn(), btnClasses: this.btnClasses(), }) ); get vm() { return this.viewModel(); } }I use
viewChildrento query a group of buttons with a template reference namedel. When a button is clicked, thelastClickedBtnSignal stores the current button ID.Then, the
btnClassescomputed signal derives the CSS class of the button according tobtnGroupsandlastClickedBtn. When the button and thelastClickedBtnsignal have the same ID, change the background color of the button to blue. Otherwise, the button has the default button color.<ng-template #t let-id> <p>Simple Template {{ id }}</p> </ng-template> t = viewChild.required('t', { read: TemplateRef });Then, I use
viewChildto query a TemplateRef with a template reference namedt. The simple template displays "Simple Template" and the ID of the clicked button.<ng-container *ngTemplateOutlet="vm.template; context: { $implicit: lastClickedBtn() }" />The
ngTemplateOutletdirective renders the template and displays the value oflastClickedBtnSignal.Demo 3: use contentChild and contentChildren to select a dynamic template
// app-contentchild.component.ts @Component({ selector: 'app-wrapper', standalone: true, imports: [NgTemplateOutlet], template: ` <div> <ng-content select=".title" /> <ng-content select=".btn" /> </div> <ng-container *ngTemplateOutlet="t; context: { $implicit: buttonId() }" /> <ng-template #t let-data> <p>Simple Template {{ data }}</p> </ng-template> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppWrapper implements AfterContentInit { title = contentChild<ElementRef<HTMLParagraphElement>>('title'); btnGroups = contentChildren<ElementRef<HTMLButtonElement>>('btn'); buttonId = input.required<number>(); constructor() { effect(() => { this.setBackgroundColor(this.buttonId()); }); } ngAfterContentInit(): void { const title = this.title(); if (title) { const { nativeElement: { style } } = title; style.fontWeight = 'bold'; style.fontStyle = 'italic'; style.fontSize = '24px'; } } setBackgroundColor(buttonId: number) { this.btnGroups().map((b) => { const element = b.nativeElement; const id = +(element.dataset['id'] || '1') const className = buttonId === id ? 'last-clicked' : ''; element.className = className; }); } }
AppWrapperis a component that consists of twong-contentelements to display a title and a group of buttons. I usedcontentChildto query the title Signal and change its CSS styling in thengAfterContentInitmethod. I also usedcontentChildrento query a group of buttons and derive their CSS class in thesetBackgroundColormethod. When the buttonId signal input and the button have the same ID, the button has a blue background color. Otherwise, the button has the default background color.// app-contentchild.component.ts @Component({ selector: 'app-contentchild', standalone: true, imports: [AppWrapper], template: ` <div class="container"> <p>contentchild and contentchildren demo</p> <app-wrapper [buttonId]="vm.clickedId"> <p class="title" #title>Select a template</p> <button #btn class="btn" data-id="1" (click)="clickedBtn.set(1)">1</button> <button #btn class="btn" data-id="2" (click)="clickedBtn.set(2)" >2</button> <button #btn class="btn" data-id="3" (click)="clickedBtn.set(3)">3</button> <button #btn class="btn" data-id="4" (click)="clickedBtn.set(4)">4</button> </app-wrapper> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppContentChild { clickedBtn = signal(1); viewModel = computed(() => ({ clickedId: this.clickedBtn(), })); get vm() { return this.viewModel(); } }
AppContentChildis a parent component that projects the title and the buttons in the AppWrapper component. When the button is clicked, it updates theclickedBtnsignal. The new signal value is passed to the input of theAppWrappercomponent to trigger the re-rendering of the template and styling of the clicked button.These are some of the examples of signal queries and I hope they are helpful to applications that regularly query components or HTML elements for styling and dynamic rendering.
The following Stackblitz repo shows the final results:
This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.
Resources:



