Write your first components — the @Component decorator, template bindings, signal inputs and outputs, @if and @for control flow, and composition with ng-content.
Why: everything on an Angular screen is a component — a TypeScript class with a @Component decorator that describes its tag name (selector) and markup (template). Note: generate one with (ng generate component welcome) in the terminal; components are standalone by default, no module needed.
// src/app/welcome.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-welcome', // the tag you use it as: <app-welcome />
template: `
<section>
<!-- Double braces embed any expression from the class -->
<h1>Hello, {{ name }}!</h1>
<p>You know {{ frameworks }} frameworks.</p>
</section>
`,
})
export class Welcome {
// Class fields are what the template reads
name = 'Ada'
frameworks = 2 + 1
}Why: the template looks like HTML plus two additions you will use constantly — [square brackets] to bind an attribute to a class value, and (parentheses) to listen to an event. Learn these two and most templates read themselves.
import { Component } from '@angular/core'
@Component({
selector: 'app-bindings',
template: `
<!-- [property] binds a class value, not a string -->
<img [src]="imageSrc" alt="Logo" />
<button [disabled]="isDisabled">Can't click me</button>
<!-- (event) listens — the () after save means "call this method" -->
<button (click)="save()">Save</button>
<!-- Any expression works inside a binding -->
<p [class]="isDisabled ? 'muted' : 'active'">Status</p>
`,
})
export class Bindings {
imageSrc = '/photo.svg'
isDisabled = true
save() {
console.log('saved at', Date.now())
}
}Why: inputs are how a parent passes data down to a child — Angular's props. input() creates a signal input, read by CALLING it: label(). They are read-only — a component never modifies its own inputs. Note: input.required has no default; input(0) falls back to 0 when the parent omits it.
import { Component, input } from '@angular/core'
@Component({
selector: 'app-badge',
// Read an input by calling it: label()
template: `<span>{{ label() }}: {{ count() }}</span>`,
})
export class Badge {
label = input.required<string>() // parent MUST pass this
count = input(0) // 0 is the fallback when the parent omits it
}
@Component({
selector: 'app-header',
imports: [Badge], // list the components this template uses
template: `
<header>
<!-- Static strings need no brackets; values use [brackets] -->
<app-badge label="Inbox" [count]="3" />
<app-badge label="Drafts" /> <!-- count falls back to 0 -->
</header>
`,
})
export class Header {}Why: inputs go down, outputs come up. A child announces that something happened with output().emit(…), and the parent listens with (eventName) — the child never reaches into the parent directly. $event is whatever the child emitted.
import { Component, output } from '@angular/core'
@Component({
selector: 'app-add-button',
template: `<button (click)="add.emit('New task')">Add task</button>`,
})
export class AddButton {
// The event this component can send up, with its payload type
add = output<string>()
}
@Component({
selector: 'app-tasks',
imports: [AddButton],
template: `
<!-- $event is whatever the child emitted -->
<app-add-button (add)="handleAdd($event)" />
`,
})
export class Tasks {
handleAdd(text: string) {
console.log('child sent:', text)
}
}Why: components constantly show different UI for different data — signed in or not, empty or full, admin or member. @if / @else if / @else blocks live right in the template, and plain ternaries inside braces cover the small choices.
import { Component } from '@angular/core'
type User = { name: string; unread: number; isAdmin: boolean }
@Component({
selector: 'app-inbox',
template: `
@if (!user) {
<p>Please sign in.</p>
} @else {
<!-- Ternaries work inside braces for small choices -->
<h2>{{ user.isAdmin ? 'Admin Console' : 'Your Inbox' }}</h2>
@if (user.unread > 0) {
<p>{{ user.unread }} unread messages</p>
}
}
`,
})
export class Inbox {
user: User | null = { name: 'Ada', unread: 2, isAdmin: false }
}Why: rendering arrays is a @for block plus a track expression. track tells Angular which item is which between updates, so it can reorder instead of rebuilding — use a stable id from your data. Note: @empty renders when the list has nothing in it; no separate empty-check needed.
import { Component } from '@angular/core'
type Task = { id: number; title: string; done: boolean }
@Component({
selector: 'app-task-list',
template: `
<ul>
<!-- track must be stable and unique — use the data's id -->
@for (task of tasks; track task.id) {
<li>{{ task.done ? '[x]' : '[ ]' }} {{ task.title }}</li>
} @empty {
<li>No tasks yet.</li>
}
</ul>
`,
})
export class TaskList {
tasks: Task[] = [
{ id: 1, title: 'Learn templates', done: true },
{ id: 2, title: 'Master signals', done: false },
{ id: 3, title: 'Ship an app', done: false },
]
}Why: instead of components with dozens of inputs, build a frame and let callers fill it. <ng-content /> renders whatever the caller nests between the tags — this is the pattern behind every Card, Layout, and Modal you will ever write.
import { Component, input } from '@angular/core'
@Component({
selector: 'app-card',
template: `
<section class="card">
<h3>{{ title() }}</h3>
<!-- Renders whatever the caller nests between the tags -->
<ng-content />
</section>
`,
})
export class Card {
title = input.required<string>()
}
@Component({
selector: 'app-dashboard',
imports: [Card],
template: `
<app-card title="Revenue">
<p>$12,400 this month</p>
<button>Details</button>
</app-card>
`,
})
export class Dashboard {}