Skip to main content

viewChild & contentChild

viewChild() - Signal-based Queries

Access child component/element/directive in the view (template) using signals.

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
selector: 'app-parent',
standalone: true,
template: `
<input #nameInput placeholder="Enter name">
<app-child #childComp></app-child>
<button (click)="focusInput()">Focus Input</button>
`
})
export class ParentComponent {
// Signal-based query - NO lifecycle hooks needed!
nameInput = viewChild<ElementRef<HTMLInputElement>>('nameInput');

// Query by component type
childComponent = viewChild(ChildComponent);

focusInput() {
// Access as signal - automatically available
this.nameInput()?.nativeElement.focus();
}

callChild() {
this.childComponent()?.doSomething();
}
}

Key benefits:

  • ✅ No ngAfterViewInit needed
  • ✅ Returns a signal - call with ()
  • ✅ Automatically undefined-safe
  • ✅ Reactive by default

viewChild.required() - Required Queries

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
selector: 'app-form',
standalone: true,
template: `
<input #emailInput type="email" required>
<app-submit-button></app-submit-button>
`
})
export class FormComponent {
// Required query - compile error if not found
emailInput = viewChild.required<ElementRef<HTMLInputElement>>('emailInput');
submitButton = viewChild.required(SubmitButtonComponent);

submit() {
// No need for ? - guaranteed to exist
const email = this.emailInput().nativeElement.value;
this.submitButton().disable();
}
}

Benefits:

  • Compile-time checking
  • No ! (non-null assertion) needed
  • TypeScript knows it exists

viewChildren() - Multiple Elements

import { Component, viewChildren } from '@angular/core';

@Component({
selector: 'app-list',
standalone: true,
template: `
<app-item *ngFor="let item of items" [data]="item"></app-item>
<p>Total items: {{ itemComponents().length }}</p>
`
})
export class ListComponent {
items = ['A', 'B', 'C'];

// Signal returning array of components
itemComponents = viewChildren(ItemComponent);

logItems() {
console.log('Items:', this.itemComponents().length);

// Iterate over items
this.itemComponents().forEach(item => {
item.doSomething();
});
}

getFirst() {
return this.itemComponents()[0];
}
}

Returns:

  • Signal containing array of matching elements
  • Empty array if none found
  • Automatically updates when children change

contentChild() - Projected Content

import { Component, contentChild } from '@angular/core';

@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<!-- Projected header -->
<ng-content select="[card-title]"></ng-content>
</div>
<div class="card-body">
<!-- Projected body -->
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {
// Query projected content
title = contentChild(CardTitleComponent);

logTitle() {
const titleComp = this.title();
if (titleComp) {
console.log('Title:', titleComp.text());
}
}
}

@Component({
selector: 'app-card-title',
standalone: true,
template: `<h3>{{ text() }}</h3>`
})
export class CardTitleComponent {
text = input<string>('');
}

contentChildren() - Multiple Projected

import { Component, contentChildren, effect } from '@angular/core';

@Component({
selector: 'app-tabs',
standalone: true,
template: `
<div class="tabs">
<ng-content></ng-content>
</div>
<p>Total tabs: {{ tabs().length }}</p>
`
})
export class TabsComponent {
// Query all projected TabComponent
tabs = contentChildren(TabComponent);

constructor() {
// Effect runs when tabs() changes
effect(() => {
const allTabs = this.tabs();
console.log('Tabs count:', allTabs.length);

// Activate first tab
if (allTabs.length > 0) {
allTabs[0].activate();
}
});
}
}

@Component({
selector: 'app-tab',
standalone: true,
template: `<div>Tab content</div>`
})
export class TabComponent {
isActive = signal(false);

activate() {
this.isActive.set(true);
}
}

Query Options

Read as Different Type

import { Component, viewChild, ElementRef, ViewContainerRef } from '@angular/core';

@Component({
template: `<div #container>Content</div>`
})
export class ReadComponent {
// Read as ElementRef (default for elements)
containerEl = viewChild<ElementRef>('container');

// Read as ViewContainerRef
containerRef = viewChild('container', {
read: ViewContainerRef
});

useContainer() {
// Access as ElementRef
this.containerEl()?.nativeElement.style.color = 'red';

// Access as ViewContainerRef for dynamic components
this.containerRef()?.clear();
}
}

Descendants Option

@Component({
template: `
<div>
<app-child></app-child>
<div>
<app-child></app-child> <!-- Nested child -->
</div>
</div>
`
})
export class ContainerComponent {
// Query all descendants (default: true)
allChildren = viewChildren(ChildComponent, { descendants: true });

// Query only direct children
directChildren = viewChildren(ChildComponent, { descendants: false });

compare() {
console.log('All:', this.allChildren().length); // 2
console.log('Direct:', this.directChildren().length); // 1
}
}

Practical Examples

Auto-focus Input

import { Component, viewChild, ElementRef, effect } from '@angular/core';

@Component({
selector: 'app-search',
standalone: true,
template: `
<input #searchInput placeholder="Search..." />
`
})
export class SearchComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('searchInput');

constructor() {
// Auto-focus when input becomes available
effect(() => {
const input = this.searchInput();
if (input) {
input.nativeElement.focus();
}
});
}
}

Scroll to Element

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
template: `
<div class="container">
<div #targetElement>Target</div>
</div>
<button (click)="scrollToTarget()">Scroll</button>
`
})
export class ScrollComponent {
target = viewChild<ElementRef>('targetElement');

scrollToTarget() {
this.target()?.nativeElement.scrollIntoView({ behavior: 'smooth' });
}
}

Access Child Component Methods

@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal" *ngIf="isOpen()">
<ng-content></ng-content>
</div>
`
})
export class ModalComponent {
isOpen = signal(false);

open() {
this.isOpen.set(true);
}

close() {
this.isOpen.set(false);
}
}

Form Controls Management

import { Component, viewChildren, ElementRef } from '@angular/core';

@Component({
selector: 'app-multi-input',
standalone: true,
template: `
<input #input *ngFor="let field of fields" [placeholder]="field">
<button (click)="clearAll()">Clear All</button>
`
})
export class MultiInputComponent {
fields = ['Name', 'Email', 'Phone'];
inputs = viewChildren<ElementRef<HTMLInputElement>>('input');

clearAll() {
this.inputs().forEach(input => {
input.nativeElement.value = '';
});
}

focusFirst() {
this.inputs()[0]?.nativeElement.focus();
}
}

Using with computed()

import { Component, viewChildren, computed } from '@angular/core';

@Component({
selector: 'app-checklist',
standalone: true,
template: `
<app-checkbox *ngFor="let item of items" [label]="item"></app-checkbox>
<p>Checked: {{ checkedCount() }} / {{ totalCount() }}</p>
`
})
export class ChecklistComponent {
items = ['Task 1', 'Task 2', 'Task 3'];
checkboxes = viewChildren(CheckboxComponent);

totalCount = computed(() => this.checkboxes().length);

checkedCount = computed(() => {
return this.checkboxes().filter(cb => cb.isChecked()).length;
});
}

Using with effect()

import { Component, contentChildren, effect } from '@angular/core';

@Component({
selector: 'app-accordion',
standalone: true,
template: `<ng-content></ng-content>`
})
export class AccordionComponent {
panels = contentChildren(PanelComponent);

constructor() {
// React to panel changes
effect(() => {
const allPanels = this.panels();
console.log('Panel count:', allPanels.length);

// Ensure only one is open
const openPanels = allPanels.filter(p => p.isOpen());
if (openPanels.length > 1) {
// Close all but first
openPanels.slice(1).forEach(p => p.close());
}
});
}
}

viewChild vs contentChild

FeatureviewChild()contentChild()
LocationComponent's templateProjected content (<ng-content>)
Use caseAccess own childrenAccess projected children
ExampleElements in templateChildren passed via <ng-content>
ReturnsSignal with element/componentSignal with element/component
AvailableImmediately (as signal)Immediately (as signal)

Best Practices

  • ✅ Use viewChild() instead of @ViewChild() - Signal-based, reactive
  • ✅ Use viewChild.required() for elements that must exist
  • ✅ Use optional chaining: this.element()?.method()
  • ✅ Use viewChildren() for lists - returns signal with array
  • ✅ Use contentChild() for projected content
  • ✅ Combine with computed() for derived values
  • ✅ Use effect() to react to query changes
  • ✅ Use { read: ViewContainerRef } for dynamic components
  • ❌ Don't use ngAfterViewInit with signal queries
  • ❌ Don't forget () - they are signals!

Signal Queries vs Decorator Queries

Feature@ViewChild() (Old)viewChild() (Modern)
TypePropertySignal
Accessthis.elementthis.element()
AvailableAfter ngAfterViewInitImmediately
ReactivityManualAutomatic
Required@ViewChild({ ... })!viewChild.required()
Multiple@ViewChildren() + QueryListviewChildren() returns array
Safe accessManual ? or !Automatic with ()?.
Works withLifecycle hooksSignals, computed(), effect()

Common Patterns

Pattern 1: Conditional Element

modal = viewChild(ModalComponent);  // Optional

showModal() {
this.modal()?.open(); // Safe - might not exist
}

Pattern 2: Required Element

form = viewChild.required<ElementRef>('form');  // Must exist

submitForm() {
this.form().nativeElement.submit(); // No ? needed
}

Pattern 3: Multiple Elements

items = viewChildren(ItemComponent);

processAll() {
this.items().forEach(item => item.process());
}

Pattern 4: Derived State

checkboxes = viewChildren(CheckboxComponent);
allChecked = computed(() =>
this.checkboxes().every(cb => cb.isChecked())
);