Signal queries in Angular – what can I do with them?

railsstudent

Connie Leung

Posted on February 15, 2024

Signal queries in Angular – what can I do with them?

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

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

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

The template-driven form has a template reference f that viewChild uses to obtain a reference to NgForm. When a user clicks the submit button, the data is printed inside the <pre> block.

Manipulate form state with viewChild

// app-simple-form.component.ts

form = viewChild.required('f', { read: NgForm });
The demo uses the viewChild to query a NgForm with a template reference named f. The result is NgForm that is assigned to form member.

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

formValues is a Signal that stores the form data when it changes. isFormValid is a Signal that stores whether or not a form is valid. isFormSubmitted is a boolean Signal that sets to true when a user clicks the submit button.

viewModel is a computed signal that represents the view model of the component. Finally, I define a vm getter to return the value of the viewModel signal.

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

When the form values change, effect is run to update formValues, isFormValid, and isFormSubmitted Signals respectively. Then, viewModel is recomputed and it updates the vm getter. The button is disabled when vm.isFormValid is false. Similarly, when vm.isFormSubmitted is true, the form values appear within the <pre> block.

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

I use viewChildren to query a group of buttons with a template reference named el. When a button is clicked, the lastClickedBtn Signal stores the current button ID.

Then, the btnClasses computed signal derives the CSS class of the button according to btnGroups and lastClickedBtn. When the button and the lastClickedBtn signal 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 });
Enter fullscreen mode Exit fullscreen mode

Then, I use viewChild to query a TemplateRef with a template reference named t. The simple template displays “Simple Template” and the ID of the clicked button.

<ng-container *ngTemplateOutlet="vm.template; context: { $implicit: lastClickedBtn() }" />
Enter fullscreen mode Exit fullscreen mode

The ngTemplateOutlet directive renders the template and displays the value of lastClickedBtn Signal.

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

AppWrapper is a component that consists of two ng-content elements to display a title and a group of buttons. I used contentChild to query the title Signal and change its CSS styling in the ngAfterContentInit method. I also used contentChildren to query a group of buttons and derive their CSS class in the setBackgroundColor method. 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

AppContentChild is a parent component that projects the title and the buttons in the AppWrapper component. When the button is clicked, it updates the clickedBtn signal. The new signal value is passed to the input of the AppWrapper component 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:

💖 💪 🙅 🚩
railsstudent
Connie Leung

Posted on February 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related