-). For example, a component named UserProfile has a file name user-profile.ts..spec.ts.
For example, the unit test file for the UserProfile component has the file name user-profile.spec.ts.src.src directory.main.ts.components/, directives/, and services/.inject() function over constructor parameter injection.protected on class members that are only used by a component's template.@Component({
...,
template: `{{ fullName() }}
`,
})
export class UserProfile {
firstName = input();
lastName = input();
// `fullName` is not part of the component's public API, but is used in the template.
protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}
readonly for properties that shouldn't change.@Component({/* ... */})
export class UserProfile {
readonly userId = input();
readonly userSaved = output();
readonly userName = model();
}
signal is a reactive value container that automatically notifies Angular when its content changes.import { signal, Component } from '@angular/core';
@Component({
selector: 'counter-example',
template: `
<h3>Count: {{ count() }}</h3>
<button (click)="increment()">+</button>
`,
standalone: true,
})
export class CounterExample {
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
}
count(), not count.
signal(). It can be changed with:
.set(newValue) – replace the value directly.update(fn) – change it based on its current valueconst counter = signal(0);
counter.set(10); // sets value to 10
counter.update(v => v+1); // now 11
console.log(counter()); // prints 11
import { signal, computed } from '@angular/core';
const first = signal('Hwang');
const last = signal('Fu');
const fullName = computed(() => `${first()} ${last()}`);
console.log(fullName()); // "Hwang Fu"
last.set('Fucius');
console.log(fullName()); // "Hwang Fucius" – updated automatically
effect() runs a piece of code automatically whenever the signals it reads change.
Useful for logging, animations, or syncing values.
import { signal, effect } from '@angular/core';
const count = signal(0);
effect(() => {
console.log('Count changed:', count());
});
count.set(1); // console logs: Count changed: 1
count.set(2); // console logs: Count changed: 2
input().
@Component({
selector: 'user-card',
template: `<p>Hello, {{ name() }}!</p>`,
})
export class UserCard {
name = input('Anonymous'); // signal-style input with default
}
input() → for reading input signalsoutput() → for emitting eventsmodel() → for two-way binding (v18+)readonly by design, meaning you cannot call .set() or .update() on it.signal()) can both be read and updated.
<h2>{{ count() }}</h2> <!-- ✅ correct -->
<h2>{{ count }}</h2> <!-- ❌ won't update -->
BehaviorSubject for simple caseslinkedSignal() is a special kind of signal that stays connected to another signal.import { signal, linkedSignal } from '@angular/core';
const name = signal('Hwangfu');
// Linked signal that always follows `name`
const upperName = linkedSignal(() => name().toUpperCase());
console.log(upperName()); // "HWANGFU"
name.set('Fucius');
console.log(upperName()); // "FUCIUS" – updated automatically
computed()?computed() and linkedSignal() look similar, but there's one key difference:computed() → read-only, you can't change its value directlylinkedSignal() → can also be written to (you can modify it)const count = signal(0);
// Computed: only reads from count
const double = computed(() => count() * 2);
// Linked: can read and write
const mirror = linkedSignal(() => count(), {
// define how writing back should work
set: (newVal) => count.set(newVal),
});
mirror.set(5);
console.log(count()); // 5
const activeUser = signal({ id: 1, name: 'Alice' });
// Create a linked version for editing
const editUser = linkedSignal(() => activeUser(), {
// treat users with the same id as the same object
equal: (a, b) => a.id === b.id,
});
console.log(editUser()); // { id: 1, name: 'Alice' }
editUser.update(u => ({ ...u, name: 'Alicia' }));
console.log(editUser().name); // "Alicia"
// if `activeUser` changes, `editUser` also updates
activeUser.set({ id: 1, name: 'Alina' });
console.log(editUser().name); // "Alina"
const copy = linkedSignal(() => source());
2. Detailed form
const copy = linkedSignal({
source: source,
computation: (value) => value,
equal: (a, b) => a.id === b.id,
});
@Component({ selector: 'button[type="reset"]' }), Angular matches all elements in the DOM that fit this CSS selector.@Component({
selector: 'button[type="reset"]',
template: ' ',
})
export class ResetButton {}
<button type="reset"> becomes a ResetButton component host.ResetButton component that:
primary / ghost)loading state (shows a spinner, disables clicks)aria-disabled, aria-busy)<ng-content>import { Component, HostBinding, HostListener, Input } from '@angular/core';
@Component({
selector: 'button[type="reset"]',
// The host element IS the <button>. We only render inside it.
template: `
<span class="content" [class.hidden]="loading">
<ng-content></ng-content>
</span>
<span class="spinner" *ngIf="loading" aria-hidden="true">⏳</span>
`,
styles: [`
:host {
/* base look */
font: inherit;
border-radius: 6px;
padding: .5rem .9rem;
cursor: pointer;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
gap: .5rem;
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
/* variants via classes on :host */
:host(.primary) {
background: #0d6efd;
color: white;
}
:host(.primary:hover) { background: #0b5ed7; }
:host(.ghost) {
background: transparent;
color: #0d6efd;
border-color: #0d6efd;
}
:host(.ghost:hover) { background: rgba(13,110,253,.08); }
/* disabled look */
:host(.disabled) {
opacity: .55;
cursor: not-allowed;
pointer-events: none;
}
.spinner { display: inline-flex; }
.content.hidden { visibility: hidden; } /* reserve space so width doesn't jump */
`],
standalone: true,
})
export class ResetButton {
/** Visual variant */
@Input() variant: 'primary' | 'ghost' = 'primary';
/** Loading state: blocks clicks, shows spinner, updates aria */
@Input() loading = false;
/** Disable state (useful if you want to disable the reset button) */
@Input() disabled = false;
/** Reflect variant and states as CSS classes on the host <button> */
@HostBinding('class')
get hostClasses(): string {
const v = this.variant ?? 'primary';
return [v, this.disabled || this.loading ? 'disabled' : ''].filter(Boolean).join(' ');
}
/** Keep native semantics but add A11y hints */
@HostBinding('attr.aria-disabled') get ariaDisabled() { return String(this.disabled); }
@HostBinding('attr.aria-busy') get ariaBusy() { return String(this.loading); }
/** Optional: ensure the host has type="reset" (safety if HTML missed it) */
@HostBinding('attr.type') readonly type = 'reset';
/** Block clicks when disabled/loading */
@HostListener('click', ['$event'])
onClick(ev: MouseEvent) {
if (this.disabled || this.loading) {
ev.preventDefault();
ev.stopImmediatePropagation();
}
}
}
<!-- Matches selector because it's a button[type="reset"] -->
<button type="reset" variant="primary">Reset form</button>
<!-- Switch variant -->
<button type="reset" variant="ghost">Clear</button>
<!-- Loading state -->
<button type="reset" variant="primary" [loading]="true">Resetting…</button>
<!-- Disabled state -->
<button type="reset" [disabled]="true">Disabled reset</button>
<!-- With projected rich content -->
<button type="reset" variant="primary">
<span class="hl-grey-fg">↺</span>
Reset filters
</button>
@Input() values (variant, loading, disabled) so templates can control the button.@HostBinding adds/removes CSS classes on the host <button>.@HostListener('click') prevents clicks when disabled/loading.type="reset" is enforced, so even if HTML forgot it, it behaves as a reset button.selector: 'drop-zone, [dropzone]' mean?<drop-zone></drop-zone>dropzone attribute, like <div dropzone>DropZone component.@Component({
selector: 'drop-zone, [dropzone]',
template: ' ',
})
export class DropZone {}
@Input().@Component({
selector: 'drop-zone',
template: ' ',
})
export class DropZone {
@Input() mode: 'files' | 'links' | 'images' = 'files';
}
Usage:
<drop-zone></drop-zone> (uses default: mode = "files")
<drop-zone mode="links"></drop-zone> (sets mode = "links")
@HostBinding('attr.<attribute-name>').@Directive({
selector: '[dropzone]',
})
export class DropzoneDirective {
@Input() dropzone: string = 'files';
@HostBinding('attr.dropzone')
get attrValue() {
return this.dropzone;
}
}
Resulting DOM:
<div dropzone="files"></div>
booleanAttribute transform.appResizable behave like a proper boolean.@Directive({
selector: '[appResizable]',
})
export class ResizableDirective {
@Input({ alias: 'appResizable', transform: booleanAttribute })
resizable = true;
@HostBinding('attr.data-resizable')
get data() {
return String(this.resizable);
}
}
Usage:
<div appResizable></div> (true by default)
<div appResizable="true"></div> (true)
<div [appResizable]="false"></div> (false)
type="reset" for any button with a directive.
@Directive({
selector: 'button[appResetDefault]',
})
export class ResetDefaultDirective {
@HostBinding('attr.type') type = 'reset';
}
<button appResetDefault>Clear</button>
This becomes:
<button type="reset">Clear</button>
selector tells Angular where to place a component in the DOM.@Component({ ... }) metadata:@Component({
selector: 'app-user-card',
template: `<p>User card works</p>`,
})
export class UserCard {}
<app-user-card> in a template, it creates a UserCard component there.@Component({
selector: 'app-profile',
template: `<h2>Profile</h2>`,
})
export class ProfileComponent {}
<!-- Usage in another template -->
<app-profile></app-profile>
app-profile, user-card, etc.app- (you can customize your own) to avoid conflicts with future HTML tags.@Component({
selector: '[appPanel]',
template: `<ng-content></ng-content>`,
})
export class PanelComponent {}
<!-- Usage -->
<section appPanel>
Panel content
</section>
<section> element.@Component({
selector: 'drop-zone, [dropzone]',
template: '<ng-content></ng-content>',
})
export class DropZoneComponent {}
<!-- Both usages match the same component -->
<drop-zone>Drop files here</drop-zone>
<div dropzone>Drop files here</div>
<drop-zone> and any element with the dropzone attribute become hosts for the same component.selector field accepts valid CSS selectors.@Component({
selector: 'button[type="reset"]',
template: '<ng-content></ng-content>',
})
export class ResetButtonComponent {}
<button type="reset">Reset form</button>
<button type="reset"> now becomes a ResetButtonComponent host.div or button, they can match too many elements and make templates confusing.app-, admin-, shared-).
app-header, app-footer, app-user-card
[appPanel], [appResetDefault], [dropzone]
@Component({
selector: 'app-user-card',
template: `<div class="card">User card</div>`,
styles: [`
.card {
padding: 1rem;
border-radius: .5rem;
border: 1px solid #ddd;
}
`],
})
export class UserCard {}
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css'],
})
export class UserCard {}
styles for quick examples or tiny components.styleUrls with a .css or .scss file for anything non-trivial..title in many components without conflicts.@Component({
selector: 'app-a',
template: `<h2 class="title">A</h2>`,
styles: [`.title { color: red; }`],
})
export class AComponent {}
@Component({
selector: 'app-b',
template: `<h2 class="title">B</h2>`,
styles: [`.title { color: blue; }`],
})
export class BComponent {}
<app-a></app-a>
<app-b></app-b>
A is red and B is blue, even though both use the same class name.:host:host selector targets the outer element that holds your component.@Component({
selector: 'app-pill-badge',
template: `<ng-content></ng-content>`,
styles: [`
:host {
display: inline-block;
padding: .2rem .6rem;
border-radius: 999px;
background: #f0f4ff;
color: #1f3b8f;
font-size: .8rem;
}
`],
standalone: true,
})
export class PillBadge {}
<app-pill-badge>New</app-pill-badge>
:host styles the <app-pill-badge> element itself.:host for width, padding, display, border, etc. of the component's root element.:host(.some-class)@Component({
selector: 'app-alert',
template: `<ng-content></ng-content>`,
styles: [`
:host {
display: block;
padding: .75rem 1rem;
border-radius: 4px;
}
:host(.info) {
background: #e7f1ff;
color: #0b4f9c;
}
:host(.error) {
background: #ffe5e5;
color: #a30000;
}
`],
standalone: true,
})
export class AlertComponent {}
<app-alert class="info">Information</app-alert>
<app-alert class="error">Something went wrong</app-alert>
primary, ghost, warning, etc.<button
class="btn"
[class.btn-primary]="primary"
[class.btn-disabled]="disabled"
>
Save
</button>
@Component({
selector: 'app-save-button',
templateUrl: './save-button.component.html',
})
export class SaveButtonComponent {
primary = true;
disabled = false;
}
<div [style.opacity]="disabled ? 0.5 : 1">Content</div>
styles.css at the root of your app./* styles.css */
html, body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fafafa;
}
:host-context():host-context() lets you style a component differently depending on something outside of it.@Component({
selector: 'app-box',
template: `<ng-content></ng-content>`,
styles: [`
:host {
display: block;
padding: 1rem;
background: white;
color: black;
}
:host-context(.dark-mode) {
background: #1f2933;
color: #f9fafb;
}
`],
})
export class BoxComponent {}
<div class="dark-mode">
<app-box>This box appears in dark mode</app-box>
</div>
@Input() lets a parent component send data down to a child component.@Component({
selector: 'app-greeting',
template: `<h3>Hello, {{ name }}!</h3>`,
})
export class GreetingComponent {
@Input() name = 'Guest'; // receives value from parent
}
<!-- Parent template -->
<app-greeting name="Alice"></app-greeting>
<app-greeting name="Bob"></app-greeting>
name just like a normal HTML attribute.@Input() field.[].@Component({
selector: 'app-parent',
template: `
<app-greeting [name]="userName"></app-greeting>
`,
})
export class ParentComponent {
userName = 'Hwangfucius';
}
@Component({
selector: 'app-user-card',
template: `<p>User: {{ name }} ({{ role }})</p>`,
})
export class UserCardComponent {
@Input() name = 'Anonymous';
@Input() role = 'Viewer';
}
<app-user-card></app-user-card>
<app-user-card name="Alice" role="Admin"></app-user-card>
@Input() a string argument:@Component({
selector: 'app-book',
template: `<p>Title: {{ title }}</p>`,
})
export class BookComponent {
@Input('bookTitle') title = '';
}
<app-book bookTitle="The Hobbit"></app-book>
bookTitle, but inside the component, the field name is title.@Input() decorator.import { Component, Input, booleanAttribute } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `<button>Enabled: {{ enabled }}</button>`,
})
export class ToggleComponent {
@Input({ transform: booleanAttribute }) enabled = false;
}
<app-toggle enabled></app-toggle>
<app-toggle enabled="false"></app-toggle>
<app-toggle></app-toggle>
booleanAttribute helper makes enabled behave like a real HTML boolean attribute.true, the others are false.input().import { Component, input } from '@angular/core';
@Component({
selector: 'app-profile',
template: `<p>Welcome, {{ name() }}!</p>`,
})
export class ProfileComponent {
name = input('Guest');
}
<app-profile name="Hwangfucius"></app-profile>
@Input(), but integrates perfectly with the Angular signals system.{{ name() }}).// 1. Use a setter
@Input()
set count(value: number) {
console.log('Count changed to', value);
}
// 2. Use ngOnChanges (classic lifecycle hook)
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
ngOnChanges works if you have many inputs.@Output() lets a child component send data back to its parent.import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-like-button',
template: `<button (click)="like()">Like</button>`,
})
export class LikeButtonComponent {
@Output() liked = new EventEmitter<void>();
like() {
console.log('Button clicked!');
this.liked.emit();
}
}
<!-- Parent component -->
<app-like-button (liked)="onLiked()"></app-like-button>
(liked).@Input() works.@Component({
selector: 'app-like-counter',
template: `<button (click)="addLike()">{{ count }}</button>`,
})
export class LikeCounterComponent {
count = 0;
@Output() countChange = new EventEmitter<number>();
addLike() {
this.count++;
this.countChange.emit(this.count);
}
}
<!-- Parent component -->
<app-like-counter (countChange)="onCountUpdated($event)"></app-like-counter>
// Parent component
onCountUpdated(newCount: number) {
console.log('New like count:', newCount);
}
$event contains whatever data was emitted.this.countChange.emit(this.count).@Output().@Component({
selector: 'app-toggle',
template: `
<button (click)="toggle()">Toggle</button>
`,
})
export class ToggleComponent {
private on = false;
@Output('toggled') stateChanged = new EventEmitter<boolean>();
toggle() {
this.on = !this.on;
this.stateChanged.emit(this.on);
}
}
<!-- Parent uses the alias name -->
<app-toggle (toggled)="onToggled($event)"></app-toggle>
stateChanged, but in the template it appears as (toggled).@Component({
selector: 'app-checkbox',
template: `
<label>
<input type="checkbox" [checked]="checked" (change)="onChange($event)" />
<ng-content></ng-content>
</label>
`,
})
export class CheckboxComponent {
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
onChange(event: Event) {
const input = event.target as HTMLInputElement;
this.checkedChange.emit(input.checked);
}
}
<!-- Parent component -->
<app-checkbox [(checked)]="subscribed">Subscribe</app-checkbox>
[(checked)] syntax is shorthand for combining [checked] and (checkedChange).value / valueChange.output() instead of @Output().import { Component, output } from '@angular/core';
@Component({
selector: 'app-delete-button',
template: `<button (click)="onDelete()">Delete</button>`,
})
export class DeleteButtonComponent {
deleted = output<string>();
onDelete() {
this.deleted.emit('item-42');
}
}
<app-delete-button (deleted)="handleDelete($event)"></app-delete-button>
output() works almost the same as @Output(), but is simpler to read alongside input().<ng-content><ng-content> for this purpose.@Component({
selector: 'app-card',
template: `
<div class="card">
<h3>Header</h3>
<ng-content></ng-content> <!-- projected content here -->
</div>
`,
styles: [`
.card {
border: 1px solid #ddd;
border-radius: .5rem;
padding: 1rem;
margin: .5rem 0;
}
`],
})
export class CardComponent {}
<!-- Parent component -->
<app-card>
<p>This paragraph comes from the parent.</p>
</app-card>
<app-card> replaces <ng-content> in the child template.<ng-content> in a component, each with a CSS selector.@Component({
selector: 'app-dialog',
template: `
<div class="dialog">
<header>
<ng-content select="[dialog-title]"></ng-content>
</header>
<section>
<ng-content></ng-content> <!-- default slot -->
</section>
<footer>
<ng-content select="[dialog-actions]"></ng-content>
</footer>
</div>
`,
})
export class DialogComponent {}
<app-dialog>
<h2 dialog-title>Delete file?</h2>
<p>This action cannot be undone.</p>
<div dialog-actions>
<button>Cancel</button>
<button>Confirm</button>
</div>
</app-dialog>
<ng-content> selects the elements that match its selector.<ng-content>.ngProjectAs for flexibilityngProjectAs to "pretend" it matches a selector.<app-dialog>
<app-title ngProjectAs="[dialog-title]">Edit profile</app-title>
<p>Form fields go here...</p>
<app-footer ngProjectAs="[dialog-actions]">
<button>Close</button>
</app-footer>
</app-dialog>
<app-title> and <app-footer>, Angular projects them into the right slots.<app-card>
<app-user-info [user]="user"></app-user-info>
<app-user-stats [user]="user"></app-user-stats>
</app-card>
<app-user-info> and <app-user-stats> are rendered inside the card's <ng-content>.@Component({
selector: 'app-user-card',
template: `<p>User: {{ name }}</p>`,
})
export class UserCardComponent {
name = 'Hwangfucius';
}
<!-- Parent template -->
<app-user-card></app-user-card>
<app-user-card> tag is the host element for the UserCardComponent.<p>User: ...</p>) inside this element.<!-- In the browser -->
<app-user-card> <!-- 👈 host element -->
<p>User: Hwangfucius</p> <!-- 👈 child content -->
</app-user-card>
:host selector.@Component({
selector: 'app-alert',
template: `<ng-content></ng-content>`,
styles: [`
:host {
display: block;
padding: 1rem;
border-radius: .5rem;
background: #f9f9f9;
}
:host(.warning) {
background: #fff3cd;
color: #856404;
}
:host(.error) {
background: #f8d7da;
color: #842029;
}
`],
})
export class AlertComponent {}
<app-alert class="warning">Low battery</app-alert>
<app-alert class="error">System failure</app-alert>
:host selector targets the component's own tag — its host element.app-alert element gets styled according to its class.@HostBinding().import { Component, HostBinding, Input } from '@angular/core';
@Component({
selector: 'app-status-indicator',
template: `<ng-content></ng-content>`,
})
export class StatusIndicatorComponent {
@Input() status: 'online' | 'offline' = 'offline';
@HostBinding('class')
get hostClass(): string {
return this.status;
}
@HostBinding('attr.role') role = 'status';
}
<app-status-indicator status="online">Connected</app-status-indicator>
<app-status-indicator status="offline">Disconnected</app-status-indicator>
@HostBinding('class') dynamically sets classes on the host element.@HostBinding('attr.role') adds an accessibility attribute directly to the host.click) using @HostListener().import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-clickable',
template: `<ng-content></ng-content>`,
})
export class ClickableComponent {
@HostListener('click', ['$event'])
handleClick(event: MouseEvent) {
console.log('Host element clicked!', event);
}
}
<app-clickable>Click me!</app-clickable>
ElementRef to access it directly.import { Component, ElementRef } from '@angular/core';
@Component({
selector: 'app-box',
template: `<ng-content></ng-content>`,
})
export class BoxComponent {
constructor(private host: ElementRef<HTMLElement>) {}
ngAfterViewInit() {
this.host.nativeElement.style.border = '2px solid blue';
}
}
<app-box>Styled from code</app-box>
ElementRef gives you direct access to the DOM node — useful for low-level manipulations.<div class="wrapper"> <!-- parent element -->
<app-card></app-card> <!-- host element -->
</div>
app-card, :host styles apply to <app-card> — not to the outer <div class="wrapper">.:host-context(.dark-mode).import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-timer',
template: `<p>Seconds: {{ seconds }}</p>`,
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = 0;
private intervalId: any;
ngOnInit() {
console.log('Timer created');
this.intervalId = setInterval(() => this.seconds++, 1000);
}
ngOnDestroy() {
console.log('Timer destroyed');
clearInterval(this.intervalId);
}
}
<!-- When is created, ngOnInit runs. When it's removed, ngOnDestroy runs. -->
<app-timer *ngIf="showTimer"></app-timer>
<button (click)="showTimer = !showTimer">Toggle Timer</button>
ngOnInit() runs once when the component is first displayed.ngOnDestroy() runs right before the component is removed from the DOM.1. ngOnChanges()
2. ngOnInit()
3. ngDoCheck()
4. ngAfterContentInit()
5. ngAfterContentChecked()
6. ngAfterViewInit()
7. ngAfterViewChecked()
8. ngOnDestroy()
@Input() property changes.ngOnInit().@Component({
selector: 'app-counter',
template: `<p>Count: {{ count }}</p>`,
})
export class CounterComponent implements OnChanges {
@Input() count = 0;
ngOnChanges(changes: SimpleChanges) {
console.log('Changes:', changes);
}
}
<app-counter [count]="num"></app-counter>
<button (click)="num++">Increment</button>
SimpleChanges tells you what changed, its previous value, and new value.@Input() values.ngOnInit() {
console.log('Component ready to use!');
this.loadData();
}
ngDoCheck() {
console.log('Change detection running...');
}
<ng-content>) is first inserted, and after every subsequent check.ngAfterContentInit() {
console.log('Projected content has been initialized.');
}
ngAfterContentChecked() {
console.log('Projected content checked again.');
}
@ViewChild().@ViewChild('box') box!: ElementRef;
ngAfterViewInit() {
console.log('Template view ready:', this.box.nativeElement);
}
<div #box>Hello box</div>
ngOnInit() — they're not ready yet. Use ngAfterViewInit() instead.ngOnDestroy() {
console.log('Cleaning up...');
this.subscription.unsubscribe();
}
ngOnDestroy() is important.
┌──────────────────────────────────────┐
│ Component Created │
│ ├─ ngOnChanges() │
│ ├─ ngOnInit() │
│ ├─ ngDoCheck() │
│ ├─ ngAfterContentInit() │
│ ├─ ngAfterContentChecked() │
│ ├─ ngAfterViewInit() │
│ └─ ngAfterViewChecked() │
│ │
│ Component Updated │
│ ├─ ngDoCheck() │
│ ├─ ngAfterContentChecked() │
│ └─ ngAfterViewChecked() │
│ │
│ Component Destroyed │
│ └─ ngOnDestroy() │
└──────────────────────────────────────┘
@ViewChild() — finds one element or component inside the component's own template.@ViewChildren() — finds multiple matching elements or components in the view.@ContentChild() and @ContentChildren() — find projected content (passed in via <ng-content>).@ViewChild() to get a reference to one element or component inside your template.import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-focus-input',
template: `<input #userInput type="text" placeholder="Type something..." />`,
})
export class FocusInputComponent implements AfterViewInit {
@ViewChild('userInput') inputElement!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.inputElement.nativeElement.focus(); // focus when ready
}
}
#userInput in the template defines a template reference variable.@ViewChild('userInput') finds that element after the view is created.ngAfterViewInit(), not ngOnInit(), because the view isn't ready before that.@ViewChild() to access another component that is part of your template.@Component({
selector: 'app-child',
template: `<p>Child works!</p>`,
})
export class ChildComponent {
sayHello() {
console.log('Hello from Child!');
}
}
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<button (click)="callChild()">Call Child</button>
`,
})
export class ParentComponent {
@ViewChild(ChildComponent) child!: ChildComponent;
callChild() {
this.child.sayHello();
}
}
@ViewChild(ChildComponent) looks for the first <app-child> element in the view.@ViewChildren() when you need to handle several matching items — for example, a list of child components.QueryList that you can loop over.@Component({
selector: 'app-item',
template: `<li>Item</li>`,
})
export class ItemComponent {}
@Component({
selector: 'app-list',
template: `
<ul>
<app-item *ngFor="let i of [1,2,3]"></app-item>
</ul>
<button (click)="logItems()">Log items</button>
`,
})
export class ListComponent implements AfterViewInit {
@ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;
ngAfterViewInit() {
console.log('Found items:', this.items.length);
}
logItems() {
this.items.forEach((item, index) => console.log('Item', index + 1, item));
}
}
QueryList acts like a live array — it updates automatically if the view changes (for example, if *ngIf adds or removes elements).<ng-content>.@Component({
selector: 'app-panel',
template: `
<div class="panel">
<ng-content></ng-content>
</div>
`,
})
export class PanelComponent implements AfterContentInit {
@ContentChild('projected') content!: ElementRef;
ngAfterContentInit() {
console.log('Projected element:', this.content.nativeElement);
}
}
<app-panel>
<p #projected>Hello projected world!</p>
</app-panel>
@ContentChild() finds a matching reference inside the projected content (the part between <app-panel>...</app-panel>).ngAfterContentInit(), not ngOnInit(), since the content is only inserted after initialization.@ViewChild / @ViewChildren → for elements or components declared inside your own template. Access them in ngAfterViewInit().@ContentChild / @ContentChildren → for content projected from a parent. Access them in ngAfterContentInit().
ViewChild → single element in template
ViewChildren → multiple elements in template
ContentChild → single projected element (via <ng-content>)
ContentChildren → multiple projected elements
reset(), validate()).ElementRef is the simplest way to get a reference to a DOM element.nativeElement property.import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-box',
template: `<div #box class="blue-box">Hello Box</div>`,
styles: [`.blue-box { width: 100px; height: 100px; background: lightblue; }`]
})
export class BoxComponent implements AfterViewInit {
@ViewChild('box') box!: ElementRef<HTMLDivElement>;
ngAfterViewInit() {
console.log(this.box.nativeElement); // <div> element itself
this.box.nativeElement.style.border = '2px solid navy';
}
}
nativeElement gives direct access to the DOM node.textContent, classList, style, etc.ngAfterViewInit() so the view is ready.Renderer2 service for safer DOM manipulation.Renderer2 works even when your app runs outside the browser (like in a server environment).import { Component, ElementRef, Renderer2, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-colored-box',
template: `<div #box>Click me!</div>`
})
export class ColoredBoxComponent implements AfterViewInit {
@ViewChild('box') box!: ElementRef;
constructor(private renderer: Renderer2) {}
ngAfterViewInit() {
this.renderer.setStyle(this.box.nativeElement, 'background', 'pink');
this.renderer.setStyle(this.box.nativeElement, 'padding', '10px');
this.renderer.addClass(this.box.nativeElement, 'rounded');
}
}
setStyle(), addClass(), removeClass(), and setAttribute() are cross-platform safe.Renderer2.listen() to attach an event listener directly to an element.ngAfterViewInit() {
this.renderer.listen(this.box.nativeElement, 'click', () => {
alert('Box clicked!');
});
}
listen() automatically cleans up when the element is destroyed — you don't need to remove listeners manually.offsetWidth, offsetHeight, or getBoundingClientRect() to measure elements.ngAfterViewInit() {
const rect = this.box.nativeElement.getBoundingClientRect();
console.log('Width:', rect.width, 'Height:', rect.height);
}
scrollIntoView() and focus().scrollToBox() {
this.box.nativeElement.scrollIntoView({ behavior: 'smooth' });
}
Renderer2.listen(), Angular offers the @HostListener() decorator to respond to DOM events directly on your component's host element.import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-click-alert',
template: `<div>Click anywhere on me!</div>`,
})
export class ClickAlertComponent {
@HostListener('click')
handleClick() {
console.log('Host element clicked!');
}
}
@HostListener() automatically registers and cleans up event handlers.Renderer2.Renderer2 or @HostListener for most operations — they work in web workers and server rendering.window or document, do it only inside browser-safe conditions (e.g., check isPlatformBrowser()).import { Component } from '@angular/core';
export class BaseLogger {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
}
@Component({
selector: 'app-user',
template: `<p>User Component</p>`,
})
export class UserComponent extends BaseLogger {
constructor() {
super();
this.log('User component initialized');
}
}
@Component({
selector: 'app-admin',
template: `<p>Admin Component</p>`,
})
export class AdminComponent extends BaseLogger {
constructor() {
super();
this.log('Admin component initialized');
}
}
log() from BaseLogger.super() to use methods or constructor logic from the parent.import { OnInit, OnDestroy } from '@angular/core';
export abstract class BaseComponent implements OnInit, OnDestroy {
ngOnInit() {
console.log('Component initialized.');
}
ngOnDestroy() {
console.log('Component destroyed.');
}
}
@Component({
selector: 'app-profile',
template: `<p>Profile</p>`,
})
export class ProfileComponent extends BaseComponent {
ngOnInit() {
super.ngOnInit(); // Call base logic
console.log('Profile-specific init.');
}
}
super.ngOnInit() to keep the parent's logic.import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export class BaseDataComponent {
constructor(protected http: HttpClient) {}
fetchData(url: string) {
return this.http.get(url);
}
}
@Component({
selector: 'app-users',
template: `<button (click)="load()">Load Users</button>`,
})
export class UsersComponent extends BaseDataComponent {
load() {
this.fetchData('/api/users').subscribe(data => console.log(data));
}
}
UsersComponent inherits both the injected HttpClient and the fetchData() method.import { Directive, HostListener, ElementRef } from '@angular/core';
@Directive()
export class HoverBase {
constructor(protected el: ElementRef) {}
@HostListener('mouseenter')
onEnter() {
this.el.nativeElement.style.opacity = '0.8';
}
@HostListener('mouseleave')
onLeave() {
this.el.nativeElement.style.opacity = '1';
}
}
@Directive({
selector: '[highlightOnHover]'
})
export class HighlightDirective extends HoverBase {
@HostListener('mouseenter')
override onEnter() {
super.onEnter();
this.el.nativeElement.style.background = 'lightyellow';
}
}
HighlightDirective extends HoverBase, keeping its hover logic but adding a highlight effect.BaseLogger, you could inject a LoggerService.<app-card></app-card>.ViewContainerRef.createComponent() to insert your component at runtime.#container.<div>Dynamic Area:</div>
<ng-container #container></ng-container>
<button (click)="addCard()">Add Card</button>
<ng-container> is invisible in the DOM — it just holds a view.#container gives you a reference in TypeScript.@ViewChild() with ViewContainerRef to access the container.import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { CardComponent } from './card.component';
@Component({
selector: 'app-dynamic-host',
templateUrl: './dynamic-host.component.html',
})
export class DynamicHostComponent {
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
addCard() {
this.container.createComponent(CardComponent);
}
}
CardComponent inside the container.addCard() {
const cardRef = this.container.createComponent(CardComponent);
cardRef.instance.title = 'Dynamic card created at ' + new Date().toLocaleTimeString();
}
ComponentRef) has an instance property — that's the component itself.clearAll() {
this.container.clear();
}
const ref = this.container.createComponent(CardComponent);
// later...
ref.destroy();
@Component({
selector: 'app-alert',
template: `<div class="alert">{{ message }}</div>`,
styles: [`.alert { background: #ffe4b5; padding: .5rem; margin: .25rem 0; }`],
standalone: true,
})
export class AlertComponent {
message = '';
}
@Component({
selector: 'app-alert-demo',
template: `
<button (click)="showAlert('Something happened!')">Show Alert</button>
<ng-container #alertHost></ng-container>
`,
})
export class AlertDemoComponent {
@ViewChild('alertHost', { read: ViewContainerRef }) host!: ViewContainerRef;
showAlert(text: string) {
const ref = this.host.createComponent(AlertComponent);
ref.instance.message = text;
setTimeout(() => ref.destroy(), 3000); // auto remove after 3s
}
}
import { Type } from '@angular/core';
const components: Type<any>[] = [AlertComponent, CardComponent, WidgetComponent];
addRandom() {
const randomType = components[Math.floor(Math.random() * components.length)];
this.container.createComponent(randomType);
}
ngOnInit, ngOnDestroy, etc.).@Component() decorator can do much more than just define a template and selector.@NgModule.standalone: true.@Component({
selector: 'app-hello',
standalone: true,
template: `<p>Hello from a standalone component!</p>`,
})
export class HelloComponent {}
@Component({
selector: 'app-parent',
standalone: true,
imports: [HelloComponent],
template: `<app-hello></app-hello>`,
})
export class ParentComponent {}
changeDetection.
ChangeDetectionStrategy.Default — Angular checks all components below whenever something might have changed.ChangeDetectionStrategy.OnPush — Angular only re-checks when input references change or an event happens inside the component.import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-optimized',
template: `<p>Optimized Component</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedComponent {}
OnPush for better performance, especially in large apps where data doesn't change often.encapsulation:ViewEncapsulation.Emulated — (default) Angular emulates Shadow DOM by adding unique attributes to selectors.ViewEncapsulation.ShadowDom — uses the browser's real Shadow DOM.ViewEncapsulation.None — no encapsulation, styles are global.import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-box',
template: `<div>I'm a box</div>`,
styles: [`div { color: red; }`],
encapsulation: ViewEncapsulation.None,
})
export class BoxComponent {}
div { color: red; } style will affect every <div> in your app — not just inside this component.None for global styles or theming, and ShadowDom for true style isolation.import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ service.count }}</p>
<button (click)="service.increment()">+</button>
`,
providers: [CounterService]
})
export class CounterComponent {
constructor(public service: CounterService) {}
}
<app-counter> gets its own instance of CounterService.ChangeDetectorRef and call its methods:import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-manual-refresh',
template: `
<p>Last updated: {{ time }}</p>
<button (click)="refresh()">Refresh</button>
`
})
export class ManualRefreshComponent {
time = new Date().toLocaleTimeString();
constructor(private cdr: ChangeDetectorRef) {}
refresh() {
this.time = new Date().toLocaleTimeString();
this.cdr.detectChanges(); // manually trigger view update
}
}
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-box',
template: `<ng-content></ng-content>`,
})
export class BoxComponent {
@HostBinding('class.active') isActive = false;
@HostListener('click')
toggle() {
this.isActive = !this.isActive;
}
}
active class on its host element.imports field.import { CommonModule } from '@angular/common';
@Component({
selector: 'app-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
`,
})
export class ListComponent {
items = ['A', 'B', 'C'];
}
{{ }}.@Component({
selector: 'app-greeter',
template: `
<p>Hello, {{ name }}!</p>
<p>Today is {{ date | date:'fullDate' }}.</p>
`,
})
export class GreeterComponent {
name = 'Hwangfucius';
date = new Date();
}
date) inside interpolation.disabled, checked, value, and hidden.[property]="expression".<button [disabled]="isLoading">Save</button>
<input [value]="username" />
<img [src]="imageUrl" [alt]="description" />
export class ExampleComponent {
isLoading = true;
username = 'Alice';
imageUrl = '/assets/avatar.png';
description = 'Profile picture';
}
isLoading changes to false, Angular automatically re-enables the button.aria-* and data-* attributes).attr. prefix:<button [attr.aria-label]="label">Click</button>
<div [attr.data-id]="uniqueId"></div>
export class AttrExampleComponent {
label = 'Action button';
uniqueId = 12345;
}
null or undefined, the attribute is removed.[class] and [style].<div [class.active]="isActive">Active box</div>
<div [style.background-color]="color">Colored box</div>
export class StyleExampleComponent {
isActive = true;
color = 'lightgreen';
}
<div [ngClass]="{ active: isActive, highlight: hasFocus }"></div>
<div [ngStyle]="{ 'font-size': size + 'px', color: textColor }"></div>
<input [value]="username" (input)="username = $event.target.value" />
<p>Current username: {{ username }}</p>
export class TwoWayComponent {
username = 'Guest';
}
[(ngModel)] is just a shorthand for this two-way pattern.<script> or <b> are shown as text — not executed.[innerHTML] — but do it carefully.<div [innerHTML]="htmlSnippet"></div>
export class HtmlExampleComponent {
htmlSnippet = '<b>Bold text</b> and <i>italic</i>';
}
DomSanitizer.fill, cx, r).<svg width="100" height="100">
<circle [attr.cx]="50" [attr.cy]="50" [attr.r]="radius" [attr.fill]="color"></circle>
</svg>
export class SvgExampleComponent {
radius = 30;
color = 'lightblue';
}
addEventListener() manually, Angular provides a simpler, declarative way to attach events in templates using parentheses ().<element (eventName)="expression"></element>
<button (click)="onClick()">Click me</button>
export class ClickExampleComponent {
onClick() {
console.log('Button clicked!');
}
}
onClick() method in the component.$event.<input (input)="onInput($event)" placeholder="Type something..." />
export class InputExampleComponent {
onInput(event: Event) {
const input = event.target as HTMLInputElement;
console.log('You typed:', input.value);
}
}
$event gives full access to the original DOM event (the same object you'd get from addEventListener).<button (click)="sayHello('Hwangfucius', $event)">Greet</button>
export class HelloComponent {
sayHello(name: string, event: MouseEvent) {
console.log(`Hello, ${name}!`, event);
}
}
keydown, keyup, and keypress.<input (keyup.enter)="onEnter($event)" placeholder="Press Enter" />
export class KeyExampleComponent {
onEnter(event: KeyboardEvent) {
const input = event.target as HTMLInputElement;
console.log('Entered:', input.value);
}
}
.enter, .esc, .tab, .shift, .alt, and others.mouseenter, mouseleave, mousemove, mousedown, mouseup, etc.<div
(mouseenter)="onHover(true)"
(mouseleave)="onHover(false)"
[class.active]="hovered"
>
Hover over me!
</div>
export class MouseExampleComponent {
hovered = false;
onHover(isHovering: boolean) {
this.hovered = isHovering;
}
}
<input (keydown)="($event.key === 'Enter') && submit()" />
submit() runs only when the pressed key is Enter.@HostListener() decorator.import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-box',
template: `<div>Click anywhere on me</div>`,
})
export class BoxComponent {
@HostListener('click', ['$event'])
handleClick(event: MouseEvent) {
console.log('Box clicked!', event);
}
}
@HostListener() automatically handles registration and cleanup.window:scroll or document:keydown:@HostListener('window:scroll', ['$event'])
onScroll(event: Event) {
console.log('Scrolling detected!');
}
document.querySelector('button').addEventListener('click', handler);
<input [value]="text" (input)="text = $event.target.value" />
<p>You typed: {{ text }}</p>
export class CombineExampleComponent {
text = '';
}
[(ngModel)]).[value]="username" (data → UI)(input)="username = $event.target.value" (UI → data)[( )] — known as the banana-in-a-box syntax.<input [(ngModel)]="username" />
<p>Hello, {{ username }}!</p>
export class TwoWayExampleComponent {
username = 'Alice';
}
username updates instantly; when the component changes username, the input field updates automatically too.[(ngModel)]="username" is just sugar for combining two bindings:<input
[ngModel]="username"
(ngModelChange)="username = $event"
/>
[ngModel] → sets the initial value (one-way binding).(ngModelChange) → fires whenever the value changes in the UI.ngModel, you must import the FormsModule in your component or application (or add it to imports for standalone components).import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-form-demo',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name" placeholder="Enter your name" />
<p>Hello, {{ name || 'stranger' }}!</p>
`
})
export class FormDemoComponent {
name = '';
}
FormsModule, Angular will show an error:
NG0303: Can't bind to 'ngModel' since it isn't a known property...[(...)] syntax, just like ngModel.name and nameChange.
@Component({
selector: 'app-toggle',
template: `
<button (click)="toggle()">
{{ on ? 'ON' : 'OFF' }}
</button>
`,
standalone: true
})
export class ToggleComponent {
@Input() on = false;
@Output() onChange = new EventEmitter<boolean>();
toggle() {
this.on = !this.on;
this.onChange.emit(this.on);
}
}
<app-toggle [(on)]="enabled"></app-toggle>
<p>Feature is: {{ enabled ? 'Enabled' : 'Disabled' }}</p>
export class ParentComponent {
enabled = true;
}
on + onChange as a two-way binding pair.[(on)] expands into [on] and (onChange) internally.<label>
<input type="checkbox" [(ngModel)]="subscribed" /> Subscribe to newsletter
</label>
<select [(ngModel)]="language">
<option value="en">English</option>
<option value="de">German</option>
<option value="zh">Chinese</option>
</select>
<p>Subscribed: {{ subscribed }}</p>
<p>Language: {{ language }}</p>
export class PreferencesComponent {
subscribed = false;
language = 'en';
}
ngModel.import { Component, signal } from '@angular/core';
@Component({
selector: 'app-signal-demo',
template: `
<input [value]="name()" (input)="name.set($event.target.value)" />
<p>Name: {{ name() }}</p>
`,
standalone: true
})
export class SignalDemoComponent {
name = signal('Hwangfucius');
}
if, for, and switch in programming languages.*ngIf, *ngFor, and *ngSwitch.@if / @else@for@switch / @case / @default@if to render elements only when a condition is true.@if (loggedIn) {
<p>Welcome back, {{ username }}!</p>
} @else {
<p>Please log in to continue.</p>
}
export class LoginExampleComponent {
loggedIn = false;
username = 'Hwangfucius';
}
@if block conditionally includes or removes elements from the DOM.@else is optional and provides an alternate view.*ngIf, this new syntax doesn't require a wrapper element like <ng-container>.@else if just like in JavaScript.@if (role === 'admin') {
<p>Welcome, admin!</p>
} @else if (role === 'editor') {
<p>Hello, editor!</p>
} @else {
<p>Guest access only.</p>
}
export class RoleExampleComponent {
role = 'editor';
}
*ngIf blocks.@for to loop through arrays and render each item.@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
export class ListExampleComponent {
items = [
{ id: 1, name: 'Angular' },
{ id: 2, name: 'React' },
{ id: 3, name: 'Vue' },
];
}
track clause tells Angular how to identify list items efficiently (similar to trackBy in *ngFor).@for block.<ul>
@for (user of users; let i = $index; let count = $count; let even = $even) {
<li [class.even]="even">{{ i + 1 }}/{{ count }}: {{ user }}</li>
}
</ul>
export class UsersExampleComponent {
users = ['Alice', 'Bob', 'Charlie'];
}
$index — the current index (starting from 0).$count — total number of items.$even, $odd — booleans for alternating styles.@switch to choose one block to render based on a single expression.@switch (status) {
@case ('loading') {
<p>Loading...</p>
}
@case ('success') {
<p>Data loaded successfully!</p>
}
@case ('error') {
<p>Something went wrong.</p>
}
@default {
<p>Idle state.</p>
}
}
export class SwitchExampleComponent {
status = 'loading';
}
@switch works similarly to JavaScript's switch statement.@case block is rendered in the DOM.@default block is optional but recommended.@let lets you declare and reuse a local variable inside the template.@let fullName = firstName + ' ' + lastName;
<p>Hello, {{ fullName }}!</p>
export class LetExampleComponent {
firstName = 'Hwangfucius';
lastName = 'Wang';
}
@let only exists inside the template block where it's declared.<div *ngIf="loggedIn; else loggedOut">
Welcome!
</div>
<ng-template #loggedOut>Please log in.</ng-template>
<ul>
<li *ngFor="let item of items; index as i">{{ i + 1 }} - {{ item }}</li>
</ul>
*ngIf and its friends still work for backward compatibility.@if (users.length > 0) {
<ul>
@for (user of users) {
<li>{{ user }}</li>
}
</ul>
} @else {
<p>No users found.</p>
}
export class NestedExampleComponent {
users: string[] = [];
}
date, uppercase, currency), and you can also create your own custom pipes.|) symbol inside interpolation ({{ }}).{{ value | pipeName }}
{{ username | uppercase }}
username in all capital letters, without modifying the variable itself.{{ birthday | date:'longDate' | uppercase }}
date pipe formats birthday, and then uppercase converts it to capital letters.{{ amount | currency:'EUR':'symbol':'1.2-2' }}
export class PipeExampleComponent {
amount = 1234.5;
}
1234.5 as €1,234.50.:).| Pipe | Example | Description |
|---|---|---|
uppercase |
{{ 'hello' | uppercase }} |
Converts text to uppercase. |
lowercase |
{{ 'HELLO' | lowercase }} |
Converts text to lowercase. |
titlecase |
{{ 'hello world' | titlecase }} |
Capitalizes the first letter of each word. |
date |
{{ today | date:'fullDate' }} |
Formats a Date object into a readable string. |
currency |
{{ price | currency:'USD' }} |
Formats numbers as currencies. |
percent |
{{ 0.25 | percent }} |
Converts a decimal to a percentage (e.g. 0.25 → 25%). |
number |
{{ 1234.567 | number:'1.0-1' }} |
Formats numbers with control over decimal places. |
json |
{{ obj | json }} |
Pretty-prints objects as JSON text (for debugging). |
slice |
{{ list | slice:1:3 }} |
Extracts part of an array or string (start:end). |
async |
{{ user$ | async }} |
Subscribes to an Observable or Promise and shows its value automatically. |
async pipe is one of the most powerful — it automatically subscribes to Observables or Promises and updates the template when new data arrives.<p>User: {{ user$ | async }}</p>
import { Component } from '@angular/core';
import { of } from 'rxjs';
@Component({
selector: 'app-async-demo',
templateUrl: './async-demo.component.html',
})
export class AsyncDemoComponent {
user$ = of('Hwangfucius');
}
@Pipe({
name: 'myPipe',
pure: true, // default
})
export class MyPipe {
transform(value: string) {
return value.toUpperCase();
}
}
@Pipe decorator.import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'greet'
})
export class GreetPipe implements PipeTransform {
transform(name: string): string {
return `Hello, ${name}!`;
}
}
{{ 'Hwangfucius' | greet }}
PipeTransform interface and define a transform() method.@Pipe({ name: 'repeat' })
export class RepeatPipe implements PipeTransform {
transform(value: string, times: number = 2): string {
return value.repeat(times);
}
}
{{ 'Hi' | repeat:3 }}
HiHiHi.@Pipe decorator.PipeTransform interface.transform(value: any, ...args: any[]): any.import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'example' // The pipe name used in templates: {{ value | example }}
})
export class ExamplePipe implements PipeTransform {
transform(value: any): any {
// modify the value and return it
return value;
}
}
{{ 'Hello' | example }}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'greet' })
export class GreetPipe implements PipeTransform {
transform(name: string): string {
return `Hello, ${name}!`;
}
}
{{ 'Hwangfucius' | greet }}
:).@Pipe({ name: 'repeat' })
export class RepeatPipe implements PipeTransform {
transform(value: string, times: number = 2): string {
return value.repeat(times);
}
}
{{ 'Hi' | repeat:3 }}
transform() method after the first parameter.@Pipe({ name: 'capitalize' })
export class CapitalizePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value[0].toUpperCase() + value.slice(1).toLowerCase();
}
}
{{ 'aNGuLAr' | capitalize }}
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(items: string[], search: string): string[] {
if (!items || !search) return items;
const lower = search.toLowerCase();
return items.filter(item => item.toLowerCase().includes(lower));
}
}
<input [(ngModel)]="keyword" placeholder="Search..." />
<ul>
@for (name of (names | filter:keyword)) {
<li>{{ name }}</li>
}
</ul>
export class FilterExampleComponent {
keyword = '';
names = ['Alice', 'Bob', 'Charlie', 'David'];
}
pure: false.@Pipe({
name: 'impureFilter',
pure: false
})
export class ImpureFilterPipe implements PipeTransform {
transform(items: string[], search: string): string[] {
// runs on every change detection
return items.filter(item => item.includes(search));
}
}
imports array.@Component({
selector: 'app-greet-demo',
standalone: true,
imports: [GreetPipe],
template: `<p>{{ 'Hwangfucius' | greet }}</p>`
})
export class GreetDemoComponent {}
greet in its template.declarations array of a module.@NgModule({
declarations: [CapitalizePipe],
exports: [CapitalizePipe]
})
export class SharedPipesModule {}
SharedPipesModule into any module that needs those pipes.<ng-content> (sorted of repeated)<ng-content> tag allows a child component to display content provided by its parent.@Component({
selector: 'app-alert',
template: `
<div class="alert">
<ng-content></ng-content>
</div>
`,
styles: [`.alert { background: #ffe5e5; padding: 1rem; border-radius: 4px; }`],
standalone: true,
})
export class AlertComponent {}
<!-- Parent component template -->
<app-alert>
Something went wrong! Please try again.
</app-alert>
<ng-content> is a placeholder where the parent's content will be inserted.select attribute to match HTML elements or CSS classes.@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-title]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`,
styles: [`
.card { border: 1px solid #ccc; border-radius: 8px; padding: 1rem; margin: .5rem 0; }
.card-header { font-weight: bold; margin-bottom: .5rem; }
`],
standalone: true,
})
export class CardComponent {}
<!-- Parent component -->
<app-card>
<span card-title>User Info</span>
<p>Name: Alice</p>
<p>Email: alice@example.com</p>
</app-card>
<span card-title> goes into the header area.<ng-content>.@Component({
selector: 'app-message-box',
template: `
<div class="message-box">
<ng-content>No message provided.</ng-content>
</div>
`,
styles: [`.message-box { border: 1px dashed #888; padding: .5rem; }`],
standalone: true,
})
export class MessageBoxComponent {}
<app-message-box></app-message-box>
<!-- Renders "No message provided." -->
<ng-content> tags acts as fallback content when the parent provides nothing.<ng-content> with Angular control flow directives like @if or @for to conditionally render projected content.@Component({
selector: 'app-section',
template: `
<div class="section">
@if (visible) {
<ng-content></ng-content>
} @else {
<p>(Hidden)</p>
}
</div>
`,
standalone: true,
})
export class SectionComponent {
visible = true;
}
visible is true.ngAfterContentInit() — runs once when the content is first inserted.ngAfterContentChecked() — runs after every update to the projected content.export class CardComponent implements AfterContentInit, AfterContentChecked {
ngAfterContentInit() {
console.log('Projected content initialized');
}
ngAfterContentChecked() {
console.log('Projected content checked');
}
}
<ng-template><ng-template> tag defines a chunk of HTML that Angular does not render right away.ngTemplateOutlet.<ng-template> until you explicitly tell it to.<ng-template>
<p>This is hidden by default.</p>
</ng-template>
*ngIf or ngTemplateOutlet.*ngIf can take an else block that points to an <ng-template>.<div *ngIf="loggedIn; else loginPrompt">
Welcome back, user!
</div>
<ng-template #loginPrompt>
<p>Please log in to continue.</p>
</ng-template>
export class LoginExampleComponent {
loggedIn = false;
}
loggedIn is false, Angular will render the loginPrompt template instead.#loginPrompt gives the template a reference name.@for loop internally uses <ng-template> to repeat DOM fragments.<ng-template #userTemplate let-name>
<li>{{ name }}</li>
</ng-template>
<ul>
@for (name of users) {
<ng-container [ngTemplateOutlet]="userTemplate" [ngTemplateOutletContext]="{ $implicit: name }"></ng-container>
}
</ul>
export class UserListComponent {
users = ['Alice', 'Bob', 'Charlie'];
}
let-name variable receives the value passed by the context ($implicit: name).ngTemplateOutlet renders the same template multiple times with different data.<ng-container> and ngTemplateOutlet.<ng-template #alertTemplate>
<div class="alert">This is an alert message!</div>
</ng-template>
<ng-container [ngTemplateOutlet]="alertTemplate"></ng-container>
ngTemplateOutlet acts like a function call — it "renders" the referenced template in that place.<ng-container> is a structural wrapper that doesn't create any extra HTML element.ngTemplateOutletContext input.let- syntax.<ng-template #greetTemplate let-person>
<p>Hello, {{ person }}!</p>
</ng-template>
<ng-container
[ngTemplateOutlet]="greetTemplate"
[ngTemplateOutletContext]="{ person: 'Hwangfucius' }">
</ng-container>
ngTemplateOutletContext as "passing parameters" to the template.<ng-template #loading><p>Loading...</p></ng-template>
<ng-template #success><p>Data loaded successfully!</p></ng-template>
<ng-template #error><p>Something went wrong.</p></ng-template>
<ng-container [ngTemplateOutlet]="currentTemplate"></ng-container>
export class TemplateSwitcherComponent {
state: 'loading' | 'success' | 'error' = 'loading';
get currentTemplate() {
if (this.state === 'success') return this.success;
if (this.state === 'error') return this.error;
return this.loading;
}
@ViewChild('loading', { static: true }) loading!: TemplateRef<any>;
@ViewChild('success', { static: true }) success!: TemplateRef<any>;
@ViewChild('error', { static: true }) error!: TemplateRef<any>;
}