Modern Angular: Signals, Standalone Components and SSR · Components, Signals and Data

Standalone Components, Templates and Control Flow

Build standalone components with signal inputs, outputs and model, render with @if, @for and @switch, and bind properties and events.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A TodoList component with a child TodoItem: the parent renders items with @for (and @empty), filters them with @switch, and the child receives data through a signal input(), reports changes with output(), and supports two-way binding with model(). The example renders the components with TestBed, Angular's testing utility, clicks buttons and prints the DOM text.

The idea

  • Components are standalone by default: each declares what its template uses in imports.
  • Template control flow: @if / @else, @for (item of items(); track item.id) with @empty, and @switch. track tells Angular how to identify items, so it reuses DOM nodes.
  • Bindings: [property]="expr", (event)="handler()", [class.done]="cond", and {{ interpolation }}.
  • input() / input.required() declare inputs as signals, output() declares events, model() a two-way bindable value ([(checked)]).

The code

Inputs, outputs, model and control flow
import { Component, computed, input, model, output, signal } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";

type Todo = { id: number; title: string; done: boolean };

@Component({
  selector: "app-todo-item",
  template: `
    <li [class.done]="done()">
      <input type="checkbox" [checked]="done()" (change)="done.set(!done())" />
      {{ todo().title }}
      <button (click)="remove.emit(todo().id)">Delete</button>
    </li>
  `,
})
class TodoItem {
  todo = input.required<Todo>();
  done = model(false);                       // two-way: [(done)]="..."
  remove = output<number>();
}

@Component({
  selector: "app-todo-list",
  imports: [TodoItem],
  template: `
    <nav>
      @for (f of filters; track f) { <button class="filter" (click)="filter.set(f)">{{ f }}</button> }
    </nav>
    <ul>
      @for (todo of visible(); track todo.id) {
        <app-todo-item [todo]="todo" [(done)]="todo.done" (remove)="delete($event)" />
      } @empty {
        <li>Nothing to show</li>
      }
    </ul>
    @switch (filter()) {
      @case ("done") { <p>Showing completed</p> }
      @case ("open") { <p>Showing open</p> }
      @default { <p>{{ remaining() }} of {{ todos().length }} left</p> }
    }
  `,
})
class TodoList {
  filters = ["all", "open", "done"] as const;
  filter = signal<"all" | "open" | "done">("all");
  todos = signal<Todo[]>([
    { id: 1, title: "Buy tea", done: false },
    { id: 2, title: "Wash teapot", done: true },
    { id: 3, title: "Invite friends", done: false },
  ]);
  visible = computed(() => this.todos().filter((t) => this.filter() === "all" || (this.filter() === "done") === t.done));
  remaining = computed(() => this.todos().filter((t) => !t.done).length);
  delete(id: number) { this.todos.update((list) => list.filter((t) => t.id !== id)); }
}

async function main() {
  TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
  const fixture = TestBed.createComponent(TodoList);
  const el: HTMLElement = fixture.nativeElement;
  const show = (label: string) =>
    console.log(`${label}: ${[...el.querySelectorAll("li, p")].map((n) => n.textContent!.replace(/\s+/g, " ").replace(" Delete", "").trim()).join(" | ")}`);
  await fixture.whenStable();
  show("initial");
  (el.querySelectorAll("li button")[0] as HTMLButtonElement).click();          // delete "Buy tea"
  await fixture.whenStable();
  show("after delete");
  (el.querySelectorAll("button.filter")[2] as HTMLButtonElement).click();      // "done" filter
  await fixture.whenStable();
  show("done filter");
  (el.querySelectorAll("button.filter")[1] as HTMLButtonElement).click();      // "open" filter
  await fixture.whenStable();
  show("open filter");
}
main();

Output

initial: Buy tea | Wash teapot | Invite friends | 2 of 3 left
after delete: Wash teapot | Invite friends | 1 of 2 left
done filter: Wash teapot | Showing completed
open filter: Invite friends | Showing open

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • @for without a meaningful track (or tracking by index for lists that change), causing DOM re-creation and lost input state.
  • Mutating input values in the child; emit an output or use model() so the parent owns the change.
  • Calling methods with heavy logic from templates; derive values with computed instead.
  • Forgetting to add child components to imports, leaving <app-todo-item> unrendered (Angular reports an unknown element).

Key points

  • Standalone components declare their dependencies in imports.
  • @if, @for (with track) and @switch replace structural directives.
  • input(), output() and model() define a component's API with signals.

Further reading: Components · Control flow · Signal inputs · Model inputs

Practice questions

What does track do in @for?

Answer

It gives each item an identity (such as its id) so Angular can keep, move or remove the matching DOM nodes when the list changes instead of re-creating them.

What's the difference between input() and model()?

Answer

input() is one-way (parent to child); model() is a writable input whose changes are propagated back to the parent through two-way binding [(value)].

What does a standalone component's imports array contain?

Answer

The components, directives and pipes its template uses; only those are available in that template.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?