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.
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.tracktells 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
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
@forwithout a meaningfultrack(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
computedinstead. - 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.