Signal Queries (v19)

📚 Signal-based Queries

Signal queries replace decorator-based queries with reactive signals:

  • viewChild(): Query single element in view (replaces @ViewChild)
  • viewChildren(): Query multiple elements (replaces @ViewChildren)
  • contentChild(): Query projected content (replaces @ContentChild)
  • contentChildren(): Query multiple projected (replaces @ContentChildren)
  • Signal-based: Results are signals, reactive to changes
  • No need for ngAfterViewInit: Use computed/effect instead

🎯 Interview Questions

  • Q1: What is the advantage of signal queries over decorator queries?
  • A: Signal queries are reactive - you can use them in computed() and effect(). No need for ngAfterViewInit timing issues.
  • Q2: What does viewChild.required() return?
  • A: A signal guaranteed to have a value. Throws error if element not found.
  • Q3: When are signal query results available?
  • A: After view initialization, just like decorators. But with signals, you use computed/effect instead of lifecycle hooks.
  • Q4: Can you query by template reference variable?
  • A: Yes! viewChild('myRef') or viewChild.required('myRef') queries #myRef.

🔧 viewChild() Demo

Query by Template Reference

Button uses viewChild('searchInput') to access the input element

Query by Component Type

First Panel

First PanelItem component queried via viewChild(PanelItemComponent)

🔧 viewChildren() Demo

Query Multiple Elements

Item 1
Item 2
Item 3

Panel count (from viewChildren signal): 0

📋 Query Types Reference

Old (Decorator)New (Signal)Return Type
@ViewChild('ref')viewChild('ref')Signal<T | undefined>
@ViewChild(Type)viewChild(Type)Signal<T | undefined>
@ViewChild('ref', { static: true })viewChild.required('ref')Signal<T>
@ViewChildren(Type)viewChildren(Type)Signal<readonly T[]>
@ContentChild(Type)contentChild(Type)Signal<T | undefined>
@ContentChildren(Type)contentChildren(Type)Signal<readonly T[]>

💻 Signal Queries Code


import { 
  viewChild, 
  viewChildren, 
  contentChild, 
  contentChildren,
  computed,
  effect 
} from '@angular/core';

@Component({
  template: `
    <input #searchBox />
    <app-item />
    <app-item />
  `
})
export class MyComponent {
  // ===== viewChild - Single Element =====
  
  // Query by template reference
  searchBox = viewChild<ElementRef>('searchBox');
  // Returns: Signal<ElementRef | undefined>
  
  // Required - guaranteed to exist
  searchBoxRequired = viewChild.required<ElementRef>('searchBox');
  // Returns: Signal<ElementRef>
  
  // Query by component type
  firstItem = viewChild(ItemComponent);
  // Returns: Signal<ItemComponent | undefined>
  
  // With read option (get ElementRef instead of component)
  itemElement = viewChild(ItemComponent, { read: ElementRef });
  
  // ===== viewChildren - Multiple Elements =====
  
  allItems = viewChildren(ItemComponent);
  // Returns: Signal<readonly ItemComponent[]>
  
  allInputs = viewChildren<ElementRef>('input');
  // Returns: Signal<readonly ElementRef[]>
  
  // ===== contentChild - Projected Content =====
  
  // In parent template: <my-container><app-header /></my-container>
  header = contentChild(HeaderComponent);
  // Returns: Signal<HeaderComponent | undefined>
  
  headerRequired = contentChild.required(HeaderComponent);
  // Returns: Signal<HeaderComponent>
  
  // ===== contentChildren - Multiple Projected =====
  
  tabs = contentChildren(TabComponent);
  // Returns: Signal<readonly TabComponent[]>
  
  // ===== Using with computed/effect =====
  
  // Computed based on query results
  itemCount = computed(() => this.allItems().length);
  
  hasItems = computed(() => this.allItems().length > 0);
  
  firstItemLabel = computed(() => 
    this.firstItem()?.label() ?? 'No item'
  );
  
  // Effect when query results change
  constructor() {
    effect(() => {
      const items = this.allItems();
      console.log('Items changed:', items.length);
    });
  }
  
  // ===== Methods using queries =====
  
  focusSearch() {
    this.searchBox()?.nativeElement.focus();
  }
  
  highlightAllItems() {
    for (const item of this.allItems()) {
      item.highlight();
    }
  }
}

// ===== No more ngAfterViewInit for queries! =====

// OLD way with decorators:
@Component({...})
export class OldComponent implements AfterViewInit {
  @ViewChild('input') input!: ElementRef;
  
  ngAfterViewInit() {
    this.input.nativeElement.focus(); // Had to wait for this hook
  }
}

// NEW way with signal queries:
@Component({...})
export class NewComponent {
  input = viewChild.required<ElementRef>('input');
  
  constructor() {
    // Use effect - runs when view is ready and input exists
    effect(() => {
      const el = this.input();
      el.nativeElement.focus();
    });
  }
}