Template Reference Variables, ng-container, ViewChild and ContentChild in Angular

Angular Tutorial: Beginner to Advanced · 6. Reusable components · 5 min read · Updated

Sometimes you need to reach an element or a child component from your code: to focus an input, read a value, or call a method on a child. Angular gives you template reference variables for the template and queries for the class.

What you will learn

  • Template reference variables with #name
  • ng-container for grouping without extra elements
  • viewChild, viewChildren, contentChild and the older @ViewChild and QueryList

Template reference variables

A #name on an element creates a variable you can use anywhere in the same template:

// src/app/refs/ref-demo.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-ref-demo',
  template: `
    <input #box type="text" placeholder="Type here" />
    <button type="button" (click)="show(box.value)">Show value</button>
    <button type="button" (click)="box.focus()">Focus</button>
    <p>You typed: {{ typed() }}</p>
  `,
})
export class RefDemo {
  protected readonly typed = signal('');

  protected show(value: string): void {
    this.typed.set(value);
  }
}

box is the real HTMLInputElement, so box.value and box.focus() work. On a component, the variable is the component instance, and #form="ngForm" gives you a directive instance by its exportAs name. This is also how you avoid the EventTarget typing error: pass box.value instead of reading $event.target.

ng-container

<ng-container> is a grouping element that does not appear in the DOM. It is useful when you need a control-flow block or a directive without adding a wrapper div:

<ul>
  @for (user of users(); track user.id) {
    <ng-container>
      <li>{{ user.name }}</li>
      <li class="meta">{{ user.email }}</li>
    </ng-container>
  }
</ul>

Two li elements per user, and no extra tag between them. With @if and @for you rarely need it, but it is also used with ngTemplateOutlet and *ngComponentOutlet.

viewChild: reach into your own template

Use viewChild() to find an element or component in the component's own template. It returns a signal:

// src/app/refs/search-box.ts
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';

@Component({
  selector: 'app-search-box',
  template: `
    <label for="q">Search</label>
    <input #query id="q" type="search" />
    <button type="button" (click)="focus()">Focus the box</button>
  `,
})
export class SearchBox {
  // A signal holding the element, or undefined until it exists.
  protected readonly query = viewChild.required<ElementRef<HTMLInputElement>>('query');

  constructor() {
    afterNextRender(() => this.focus()); // focus when the page first shows
  }

  protected focus(): void {
    this.query().nativeElement.focus();
  }
}
  • viewChild('query') looks for #query. Use .required when the element always exists.
  • The result is an ElementRef for elements. Its nativeElement is the real DOM node.
  • The signal is filled after the view is created, so read it in afterNextRender or later, not in the constructor body itself.

Call a method on a child component

// src/app/refs/counter-child.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter-child',
  template: `<p>Count: {{ count() }}</p>`,
})
export class CounterChild {
  readonly count = signal(0);

  increment(): void {
    this.count.update((n) => n + 1);
  }
}
// src/app/refs/query-demo.ts
import { Component, viewChild, viewChildren } from '@angular/core';
import { CounterChild } from './counter-child';

@Component({
  selector: 'app-query-demo',
  imports: [CounterChild],
  template: `
    <app-counter-child #first />
    <app-counter-child />
    <app-counter-child />
    <button type="button" (click)="bumpFirst()">+1 first</button>
    <button type="button" (click)="bumpAll()">+1 all</button>
  `,
})
export class QueryDemo {
  protected readonly first = viewChild.required(CounterChild); // first match by type
  protected readonly all = viewChildren(CounterChild); // a signal of an array

  protected bumpFirst(): void {
    this.first().increment();
  }

  protected bumpAll(): void {
    this.all().forEach((c) => c.increment());
  }
}

viewChildren returns a signal with an array of all matches. It updates when matches are added or removed, for example inside @if or @for.

contentChild: reach into projected content

viewChild searches the component's own template. contentChild and contentChildren search the content the parent projected into <ng-content />:

// src/app/refs/tabs.ts
import { Component, contentChildren, input } from '@angular/core';

@Component({
  selector: 'app-tab',
  template: `<ng-content />`,
})
export class Tab {
  readonly title = input.required<string>();
}

@Component({
  selector: 'app-tabs',
  template: `
    <p>This container has {{ tabs().length }} tabs:</p>
    <ul>
      @for (tab of tabs(); track tab.title()) {
        <li>{{ tab.title() }}</li>
      }
    </ul>
    <ng-content />
  `,
})
export class Tabs {
  protected readonly tabs = contentChildren(Tab);
}
// src/app/refs/refs-page.ts
import { Component } from '@angular/core';
import { QueryDemo } from './query-demo';
import { RefDemo } from './ref-demo';
import { SearchBox } from './search-box';
import { Tab, Tabs } from './tabs';

@Component({
  selector: 'app-refs-page',
  imports: [QueryDemo, RefDemo, SearchBox, Tab, Tabs],
  template: `
    <app-ref-demo />
    <app-search-box />
    <app-query-demo />
    <app-tabs>
      <app-tab title="One">First</app-tab>
      <app-tab title="Two">Second</app-tab>
    </app-tabs>
  `,
})
export class RefsPage {}
// src/app/app.ts
import { Component } from '@angular/core';
import { RefsPage } from './refs/refs-page';

@Component({
  imports: [RefsPage],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <app-refs-page />
</main>

The older decorators and QueryList

Older code uses decorators. They do the same job, but return plain values and a QueryList, and are filled before the ngAfterViewInit or ngAfterContentInit hook:

// src/app/refs/legacy-queries.ts
import { AfterViewInit, Component, ElementRef, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { CounterChild } from './counter-child';

@Component({
  selector: 'app-legacy-queries',
  imports: [CounterChild],
  template: `
    <input #box />
    <app-counter-child />
    <app-counter-child />
  `,
})
export class LegacyQueries implements AfterViewInit {
  @ViewChild('box') box!: ElementRef<HTMLInputElement>;
  @ViewChildren(CounterChild) counters!: QueryList<CounterChild>;

  ngAfterViewInit(): void {
    this.box.nativeElement.focus();
    console.log('counters:', this.counters.length);
    // QueryList is observable-like: it emits when the list changes
    this.counters.changes.subscribe((list) => console.log('now', list.length));
  }
}

Migration: ng generate @angular/core:signal-queries converts decorators to signal queries.

  • `@ViewChild('x')`: New: viewChild('x')
  • `@ViewChildren(Type)`: New: viewChildren(Type)
  • `@ContentChild`: New: contentChild
  • `@ContentChildren`: New: contentChildren
  • `QueryList`: New: A plain array in a signal

When to use queries (and when not to)

Prefer data flow with inputs, outputs and signals. Use queries for things data cannot do: focusing, measuring, scrolling, or calling an imperative method of a child like video.play().

Try it yourself

  1. Add a "Select all text" button that calls select() on the input from a viewChild.
  2. Make Tabs show only the content of the active tab, using contentChildren.
  3. Use @if to remove one counter and watch all().length update.

Quick check

  1. How do you create a template reference variable?
  2. What is the difference between viewChild and contentChild?
  3. What does ng-container add to the DOM?

Next, continue with Angular Services and Dependency Injection Explained.

Frequently asked questions

Why is my viewChild undefined?

The view has not been created yet, or the element is inside an @if that is false. Read it after the first render (afterNextRender) and check for undefined unless you use .required and are sure it exists.

Can I query an element inside a child component's template?

No. Queries see only their own template and projected content. Ask the child to expose what you need.