EthicsComputerCourses
Join training +91 90532 10052

Content Projection & Queries

Angular · Lesson 11 of 41 Intermediate 7 min read Updated

Inputs pass data into a component. Sometimes you want to pass markup instead: a panel that wraps any content, a card with a title area and a footer area, a dialog with your own buttons. This is called content projection, and Angular does it with the <ng-content> element.

In the second half of this lesson you will learn queries, which let a component get hold of elements and child components in its template, for example to focus an input.

Single-slot projection

Put <ng-content /> in a component's template where the outside content should appear:

TypeScript
import { Component } from '@angular/core';

@Component({
  selector: 'app-panel',
  template: `
    <section class="panel">
      <ng-content />
    </section>
  `,
  styles: `
    .panel {
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 1rem;
    }
  `,
})
export class Panel {}

Whatever the parent writes between <app-panel> and </app-panel> is projected into that spot:

HTML
<app-panel>
  <h2>Exam timetable</h2>
  <p>Mid-term exams start on 10 November.</p>
</app-panel>

The heading and paragraph appear inside the bordered section. The projected content still belongs to the parent: its bindings use the parent's data and its styles come from the parent's CSS. The panel only decides where the content goes.

Multiple named slots

A component can have several <ng-content> elements. The select attribute takes a CSS selector and decides which pieces of content go into which slot. Content that matches no selector goes to the <ng-content> without select:

TypeScript
@Component({
  selector: 'app-notice-card',
  template: `
    <article>
      <header><ng-content select="[card-title]" /></header>
      <div class="body"><ng-content /></div>
      <footer>
        <ng-content select="[card-footer]">Posted by the office</ng-content>
      </footer>
    </article>
  `,
})
export class NoticeCard {}

The parent marks content with attributes:

HTML
<app-notice-card>
  <h3 card-title>Library closed</h3>
  <p>The library is closed on Saturday for stock-taking.</p>
  <small card-footer>Posted by Ms. Meera Nair, Librarian</small>
</app-notice-card>

<app-notice-card>
  <h3 card-title>Sports day</h3>
  <p>Register for the 100 m race by Friday.</p>
</app-notice-card>
Output
Library closed
The library is closed on Saturday for stock-taking.
Posted by Ms. Meera Nair, Librarian

Sports day
Register for the 100 m race by Friday.
Posted by the office

The second card gives no footer, so the fallback content written inside <ng-content select="[card-footer]"> appears instead. Selectors can be attributes ([card-title]), element names (button) or classes (.note).

To group several elements into one slot without adding an extra element to the page, wrap them in <ng-container card-footer>...</ng-container>. ng-container is a grouping element that renders nothing itself.

viewChild: getting an element from your template

Sometimes a component must work with a real DOM element, for example to focus a search box. Mark the element with a template reference variable and query it with viewChild():

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

@Component({
  selector: 'app-quick-search',
  template: `
    <input #searchBox placeholder="Search roll number" />
    <button (click)="clear()">Clear</button>
  `,
})
export class QuickSearch {
  private searchBox = viewChild.required<ElementRef<HTMLInputElement>>('searchBox');

  constructor() {
    afterNextRender(() => this.searchBox().nativeElement.focus());
  }

  protected clear() {
    const input = this.searchBox().nativeElement;
    input.value = '';
    input.focus();
  }
}

viewChild() returns a signal that holds the result. Its value is undefined until the view is created, so viewChild.required() is handy when the element is always there: it removes undefined from the type and throws a clear error if the element is missing. ElementRef is Angular's wrapper around a DOM element; nativeElement is the element itself.

afterNextRender runs a function once, after Angular has put the component on the screen, which is the safe moment to touch the DOM. You will learn more in Lifecycle Hooks.

You can also query a child component by its class, such as viewChild(Stopwatch), and viewChildren() returns a signal holding all matches as a read-only array.

contentChildren: inspecting projected content

viewChild looks at the component's own template. To query what the parent projected into it, use contentChild() and contentChildren(). Here an FAQ list counts its projected questions and can close them all:

TypeScript
@Component({
  selector: 'app-faq-item',
  template: `
    <button (click)="open.set(!open())">{{ question() }}</button>
    @if (open()) {
      <div><ng-content /></div>
    }
  `,
})
export class FaqItem {
  readonly question = input.required<string>();
  readonly open = signal(false);
}

@Component({
  selector: 'app-faq',
  template: `
    <p>{{ items().length }} questions, {{ openCount() }} open</p>
    <ng-content />
    <button (click)="closeAll()">Close all</button>
  `,
})
export class Faq {
  protected items = contentChildren(FaqItem);
  protected openCount = computed(() => this.items().filter((i) => i.open()).length);

  protected closeAll() {
    for (const item of this.items()) {
      item.open.set(false);
    }
  }
}

With three <app-faq-item> elements inside <app-faq>, opening the first and third shows "3 questions, 2 open". Because items is a signal, the computed count updates as items are added, removed or opened.

Template reference variables on components

For simple cases you do not even need a query. A template reference variable on a child component refers to the component instance, so the parent's template can call its public methods:

HTML
<app-stopwatch #watch />
<button (click)="watch.start()">Start</button>
<button (click)="watch.stop()">Stop</button>

Common mistakes

  • Reading a query in the constructor. The view does not exist yet, so the value is undefined (or viewChild.required throws). Read queries in templates, computed, effects or afterNextRender.
  • Expecting select to match nested elements. Only top-level projected elements are matched against select; an attribute on a deeply nested element does not count.
  • Styling projected content from the child's CSS. Projected elements belong to the parent, so style them in the parent, or use the child's :host and wrapper elements.
  • Using nativeElement for everything. Prefer bindings; reach for the DOM only for focus, measuring and third-party libraries.

Key points

  • <ng-content /> projects the parent's markup into a component; select creates named slots.
  • Content inside <ng-content> is fallback content shown when nothing is projected into that slot.
  • viewChild() and viewChildren() query the component's own template and return signals.
  • contentChild() and contentChildren() query projected content.
  • A template reference variable on a component (#watch) lets the parent template call its methods.

Exercise

Create a ProfileCard component with three slots: a photo slot ([card-photo]), a name slot ([card-name]) and a default slot for details. If no photo is given, show the text "No photo" instead. Use it twice in a parent: once for Kabir with an <img> showing /avatar.svg from your public/ folder, and once for Isha without a photo.

Show solution
TypeScript
@Component({
  selector: 'app-profile-card',
  template: `
    <div class="photo">
      <ng-content select="[card-photo]">No photo</ng-content>
    </div>
    <h3><ng-content select="[card-name]" /></h3>
    <ng-content />
  `,
})
export class ProfileCard {}
HTML
<app-profile-card>
  <img card-photo src="/avatar.svg" alt="Kabir" width="80" />
  <span card-name>Kabir Singh</span>
  <p>Class 12, Science. Cricket team captain.</p>
</app-profile-card>

<app-profile-card>
  <span card-name>Isha Verma</span>
  <p>Class 11, Commerce. Debate club.</p>
</app-profile-card>

Isha's card projects nothing into the photo slot, so the fallback text "No photo" appears there. Kabir's image goes to the photo slot because it carries the card-photo attribute.

Quick Quiz

1. Which element marks where a component shows content passed between its tags?

2. How do you send a <footer> element to a slot declared as <ng-content select="[card-footer]" />?

3. What does viewChild('box') return?

4. Which query finds child components that a parent projected into your component?

Learn Angular with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call