Signal queries in Angular – what can I do with them?
Tags: angular, tutorial, signal, frontend
Published: 2024-02-15 02:04:37

##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
```typescript
"@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
```typescript
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.

```typescript
// 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 a user clicks the submit button, the data is printed inside the &lt;pre&gt; block.

###Manipulate form state with viewChild
```typescript
// 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();
  }
```

`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.

```typescript
// 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`, `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 &lt;pre&gt; block.

###Demo 2: use viewChild and viewChildren to select a dynamic template

```typescript
// 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 `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.

```typescript
<ng-template #t let-id>
        <p>Simple Template {{ id }}</p>
</ng-template>

t = viewChild.required('t', { read: TemplateRef });
```

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.

```html
<ng-container *ngTemplateOutlet="vm.template; context: { $implicit: lastClickedBtn() }" />
```

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

###Demo 3: use contentChild and contentChildren to select a dynamic template

```typescript
// 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;
    });    
  }
}
```

`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.

```typescript
// 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();
  }
}
```

`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:

{%embed https://stackblitz.com/edit/angular-model-inputs-yqntwn?file=src%2Fmain.ts %}

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:
- Stackblitz Demo: https://stackblitz.com/edit/angular-model-inputs-yqntwn?file=src%2Fmain.ts
- Github Repo: https://github.com/railsstudent/ng-signal-queries-demo
- Github Page: https://railsstudent.github.io/ng-signal-queries-demo/