How to use @ViewChild() and @ContentChildren() parameter decorators in an Angular application to create a composite component. angular , content-children , content , children , view , child , parameter , decorator , ngTemplateOutlet , ngTemplate , ng-content , intellij , settings https://octoperf.com/blog/2022/06/07/angular-content-children/ OctoPerf ZI Les Paluds, 276 Avenue du Douard, 13400 Aubagne, France +334 42 84 12 59 contact@octoperf.com Development 1304 2022-06-08

Angular: @ViewChild() and @ContentChildren() decorators

OctoPerf is JMeter on steroids!
Schedule a Demo

With the release of OctoPerf’s new UI we wanted to create a component that would allow our users to easily edit HTTP request actions.

The new UI being heavily inspired by IDEs such as Eclipse or Visual Studio we decided to create a component that behaves likes the project settings panel of IntelliJ:

IntelliJ Settings
IntelliJ Settings

This panel displays a tree on its left part with a search input on top. The content of the left part changes depending on the current selection.

While our simplified version will only display a list on the left panel, the idea is to create a composite component :

A visual component made of disparate or separate parts or elements, here a parent settings component and children settings panel components.

Composite Component

Concretely, from a development point of view, such component usage will be like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

<lib-settings>

    <lib-settings-panel>
        <ng-container header>
            Panel 1 header
        </ng-container>
        <div>Panel 1 content</div>
    </lib-settings-panel>

    <lib-settings-panel>
        <ng-container header>
            Panel 2 header
        </ng-container>
        <div>Panel 2 content</div>
    </lib-settings-panel>

</lib-settings>

The parent component <lib-settings> contains several <lib-settings-panel>.

Each one of them defines:

  • a header displayed in the left menu,
  • a content displayed on the right when the header is selected.

How to make this happen? By using a combination of the @ViewChild and @ContentChildren parameter decorators.

Parameter decorators

Using @ViewChild in the panel component

The @ViewChild parameter decorator configures a view query, meaning that an element from the DOM can be injected into the component. The result of the query is dynamic and updated when the DOM changes.

Given the following HTML settings-panel.component.html:

1
2
3
4
5
6
7
8

<ng-template #headerTemplate>
    <ng-content select="[header]"></ng-content>
</ng-template>

<ng-template #contentTemplate>
    <ng-content></ng-content>
</ng-template>

And the settings-panel.component.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import {Component, EventEmitter, Input, Output, TemplateRef, ViewChild} from '@angular/core';

@Component({
    selector: 'lib-settings-panel',
    templateUrl: './settings-panel.component.html',
    styleUrls: ['./settings-panel.component.scss']
})
export class SettingsPanelComponent {
    @ViewChild('headerTemplate', {static: true}) header!: TemplateRef<unknown>;
    @ViewChild('contentTemplate', {static: true}) content!: TemplateRef<unknown>;

    constructor(private service: SettingsService) {
    }

    public get isSelected(): boolean {
        return this.service.isSelected(this);
    }
}

The header parameter points to the HTML <ng-template #headerTemplate> element. Here we query the DOM using the headerTemplate ID :

  • Defined in the typescript with @ViewChild(‘headerTemplate’),
  • Defined in the HTML with <ng-template #headerTemplate>.

The {static: true} configuration of the @ViewChild decorator is a performance optimization: it tells Angular to only query the DOM once and not look for updates.

The TemplateRef refers to an embedded template (the <ng-template part) that will later be used to instantiate embedded views in the parent Settings component.

The same applies for the content with the contentTemplate ID.

ng-content usage

If you are not familiar with the ng-content element it allows to inject HTML in a component. In our example we create a <lib-settings-panel> like this:

1
2
3
4
5
6
7

<lib-settings-panel>
    <ng-container header>
        Panel 1 header
    </ng-container>
    <div>Panel 1 content</div>
</lib-settings-panel>

The Panel 1 header text will be injected in the resulting HTML in place of the <ng-content select="[header]"> element and the <div>Panel 1 content</div> in place of the <ng-content> element.

Generated HTML:

1
2
3
4
5
6
7
8

<ng-template #headerTemplate>
    Panel 1 header
</ng-template>

<ng-template #contentTemplate>
    <div>Panel 1 content</div>
</ng-template>

The <ng-content> element can be used with a selector (for example select="[header]") to only inject a specific part of the HTML (here <ng-container header>) or without selector for the rest (here injecting the <div>Panel 1 content</div>).

Using @ContentChildren in the settings component

Now that we have created a child settings-panel component with a header and a content, let’s create a parent component that wraps it.

It will use the @ContentChildren parameter decorator.

The settings.component.ts component is defined as follows :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {
    AfterContentInit,
    Component,
    ContentChildren,
    EventEmitter,
    HostBinding,
    Input,
    OnDestroy,
    OnInit,
    Output,
    QueryList
} from '@angular/core';
import {SettingsService} from '@library/layout/settings/settings.service';
import {SettingsPanelComponent} from '@library/layout/settings/settings-panel/settings-panel.component';

@Component({
    selector: 'lib-settings',
    templateUrl: './settings.component.html',
    styleUrls: ['./settings.component.scss'],
    providers: [SettingsService]
})
export class SettingsComponent implements AfterContentInit, OnDestroy {
    @ContentChildren(SettingsPanelComponent) panels!: QueryList<SettingsPanelComponent>;

    private readonly subscriptions: Subscription[] = [];

    constructor(public service: SettingsService) {
    }

    ngAfterContentInit(): void {
        this._panelsChanged();
        this.subscriptions.push(this.panels.changes.subscribe(this._panelsChanged.bind(this)));
    }

    ngOnDestroy(): void {
        this.subscriptions.forEach(s => s.unsubscribe());
    }

    _panelsChanged(): void {
        // React to panels Query list update
    }
}

Children panel are injected using @ContentChildren(SettingsPanelComponent) panels!: QueryList<SettingsPanelComponent>;

While the ViewChild decorator allows to query for a single element from the view DOM, the ContentChildren decorator queries a list of elements in the content DOM.

Difference between view and content DOMs :

  • The view DOM is the HTML directly defined in the component HTML file (for example in settings-panel.component.html for our previous panel component),
  • The content DOM is the HTML defined inside the component when it is used.

In our example the content DOM is the HTML that lies inside the SettingsComponent (<lib-settings> Content DOM </lib-settings>) element. This explains the many lib-settings-panel elements, allowing us to inject them in the parent component.

Here the selector (what’s inside the parenthesis @ContentChildren(selector)) is the component SettingsPanelComponent. It could be any class with the @Component or @Directive decorators, etc. Check the API documentation for more information.

Also, we did not use the {static: true} option here, meaning that if a child Settings panel is added or removed (for example using an *ngIf), the panels query list will be updated.

Content queries are set before the ngAfterContentInit callback is called. So SettingsComponent implements AfterContentInit and we subscribe to the panels QueryList changes observable in the ngAfterContentInit method.

The _panelsChanged method would typically react to changes in the panels list. For example if a panel is removed, the currently selected one might need to be updated.

The HTML template for the settings.component.html file makes usage of the component panel :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

<div class="headers">
    <div (click)="service.select(panel)" *ngFor="let panel of panels" class="header">
        <ng-container [ngTemplateOutlet]="panel.header"></ng-container>
    </div>
</div>
<div class="content">
    <ng-template #emptyTemplate>
        No match found!
    </ng-template>
    <ng-container [ngTemplateOutlet]="service.selected?.content || emptyTemplate"></ng-container>
</div>

The left part (<div class="headers">) iterates over the list of panels and displays their headers. The [ngTemplateOutlet] directive points to the SettingsPanelComponent.header @ViewChild decorated parameter. This directive injects the content of the template in its place.

The right part (<div class="content">) displays the content of the currently selected SettingsPanel or the text ‘No match found!’ if nothing is selected.

Settings Service

The SettingsService is used to keep track of the currently selected panel (settings.service.ts) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import {Injectable} from '@angular/core';
import {SelectionModel} from '@angular/cdk/collections';
import {SettingsPanelComponent} from '@library/layout/settings/settings-panel/settings-panel.component';

@Injectable()
export class SettingsService {
    public readonly selection = new SelectionModel<SettingsPanelComponent>();

    public get selected(): SettingsPanelComponent | undefined {
        return this.selection.selected[0];
    }

    public select(panel: SettingsPanelComponent): void {
        this.selection.select(panel);
    }
}

It is provided in the SettingsComponent @Component({... providers: [SettingsService]}) export class SettingsComponent. So it is shared by both the SettingsComponent and its children SettingsPanelComponent. One instance of SettingsService is created for each <lib-settings> used in your application.

It’s a simplified version of a SettingsService : it does not handle the search field or the disabled state of the panel. This goes beyond the scope of the blog post and will be left out.

Settings panel

The complete usage of this composite component looks like this :

OctoPerf Settings
OctoPerf Settings

It handles the selection, disabled and error states, a search that highlights text in the form fields, tables, code editors, and much more.

In case you want to see it in action and/or are interested in load testing web applications, feel free to create an account on OctoPerf’s new UI.

Angular: @ViewChild() and @ContentChildren() decorators
Tags:
Share:
Be the first to comment
Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK

Want to become a super load tester?
OctoPerf Superman