1. Template Member Access
Any public member of the class can be used in the template. Private members are not accessible and will cause a TypeScript error in strict mode.
2. Standalone Components
Standalone components (standalone: true) are the default since Angular 17. A component does not need to be declared inside an NgModule.
3. String Interpolation
Output any member value as text using double curly braces:
<p>{{ username }}</p>
4. Property Binding
Binds an expression to a DOM element property (not an HTML attribute):
<img [src]="imageUrl" />
<button [disabled]="isLoading">Submit</button>
5. Attribute Binding
A separate mechanism used when no corresponding DOM property exists for an HTML attribute (e.g. ARIA attributes, colspan):
<td [attr.colspan]="colSpan"></td>
<button [attr.aria-label]="label">Click</button>
6. @Input() Decorator
The old-style @Input() decorator relies on Zone.js-based change detection (dirty checking). Angular wraps browser events via Zone.js to know when to re-run change detection.
@Input() user: User;
7. Signals
The new reactive primitive. Declare a signal and call it as a function to read its value — both in the template and inside the class:
// in the class
selectedUser = signal(DUMMY_USERS[0]);
// read inside the class
const name = this.selectedUser().name;
<!-- in the template -->
<p>{{ selectedUser().name }}</p>
8. Computed Signals
Use computed() (not compute) for derived values. A computed signal re-evaluates automatically when its dependencies change:
import { computed, signal } from '@angular/core';
selectedUser = signal(DUMMY_USERS[0]);
username = computed(() => this.selectedUser().name);
// read it like any other signal
const name = this.username();
<p>{{ username() }}</p>
9. Signal Inputs — input()
Replaces @Input(). Signal inputs are read-only — you cannot call .set() on them:
// optional with a default value
user = input('default');
// required (throws if not provided by parent)
user = input.required<User>();
// equivalent old-style decorator
@Input({ required: true }) user!: User;
Because signal inputs are read-only, this.user.set(value) is not allowed.
10. @Output() Decorator
Lets a child component notify its parent by emitting events through an EventEmitter.
Child component:
import { Component, Output, EventEmitter } from '@angular/core';
@Output() userSelected = new EventEmitter<User>();
selectedUser: User = DUMMY_USERS[0];
onSelectUser() {
this.userSelected.emit(this.selectedUser);
}
Child template:
<button (click)="onSelectUser()">Select User</button>
Parent template (listen to the event with $event to receive the emitted value):
<app-user (userSelected)="selectUser($event)" />
11. output() Function
The decorator-free replacement for @Output() + EventEmitter (Angular 17.3+). It is not a signal — it returns an OutputEmitterRef<T>, which is emit-only and cannot be read like a signal. It belongs to the same decorator-free API family as input() and model().
Child component:
import { Component, output, signal } from '@angular/core';
selectedUser = signal(DUMMY_USERS[0]);
userSelected = output<User>(); // OutputEmitterRef<User>, not a Signal
onSelectUser() {
this.userSelected.emit(this.selectedUser());
}
Child template:
<button (click)="onSelectUser()">Select User</button>
Parent template (identical to @Output() usage):
<app-user (userSelected)="selectUser($event)" />
Key differences from @Output(): no EventEmitter, no decorator. Unlike input(), you cannot call this.userSelected() to read a value — it only has .emit().
12. @for — List Rendering
Built-in control flow for rendering lists (Angular 17+). track is required and tells Angular how to identify each item across re-renders — similar to key in React. Prefer tracking by a unique id over $index when available.
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
@for also exposes implicit variables: $index, $first, $last, $even, $odd.
@for (user of users; track user.id) {
<li [class.last]="$last">{{ $index + 1 }}. {{ user.name }}</li>
}
@empty — fallback when the list is empty
Add an @empty block directly after the @for block to render fallback content when the collection is empty or null/undefined. It requires no extra *ngIf check on the list.
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
} @empty {
<li>No users found.</li>
}
Before Angular 17 — *ngFor structural directive (must be imported from CommonModule or NgFor):
<li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>
trackById(index: number, user: User) {
return user.id;
}
13. @if / @else — Conditional Rendering
Built-in control flow for conditional rendering (Angular 17+):
@if (isLoggedIn) {
<p>Welcome back, {{ username() }}!</p>
} @else {
<p>Please log in.</p>
}
You can also chain @else if:
@if (status === 'loading') {
<p>Loading...</p>
} @else if (status === 'error') {
<p>Something went wrong.</p>
} @else {
<p>{{ data }}</p>
}
Before Angular 17 — *ngIf structural directive (must be imported from CommonModule or NgIf). The else branch requires an <ng-template> with a reference variable:
<p *ngIf="isLoggedIn; else loggedOut">Welcome back!</p>
<ng-template #loggedOut><p>Please log in.</p></ng-template>
14. Class & Style Binding
Conditionally apply a CSS class based on a boolean expression:
<!-- single class -->
<div [class.active]="isActive"></div>
<!-- multiple classes via object — keys are class names, values are conditions -->
<div [ngClass]="{ active: isActive, disabled: isDisabled }"></div>
For inline styles, use [style.property] or [ngStyle]:
<div [style.color]="isError ? 'red' : 'black'"></div>
15. Two-Way Binding — ngModel
Import FormsModule in the component's imports array, then use [(ngModel)] on an input. Each input must also have a name attribute for FormsModule to track it.
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
...
})
export class MyComponent {
title = '';
}
<input [(ngModel)]="title" name="title" />
With a writable signal, [(ngModel)] does not work correctly — even though the template compiles without error (because ngModel accepts any). At runtime, [(ngModel)]="title" desugars to [ngModel]="title" + (ngModelChange)="title=$event", which passes the signal function itself to ngModel and then replaces the signal with a plain string on the first change event, destroying it. Use the split form instead:
<input [ngModel]="title()" (ngModelChange)="title.set($event)" name="title" />
Alternatively, use model() (Angular 17.2+) which natively supports two-way binding:
title = model('');
<input [(ngModel)]="title" name="title" />
Setting up custom two-way binding
Two-way binding on a custom component means the parent can both pass a value in and receive updates when the child changes it. There are two ways to set this up.
Option A — @Input() + @Output() (traditional)
The output name must be the input name suffixed with Change. This is the convention Angular uses to desugar [(value)] into [value] + (valueChange).
// child component
@Component({ selector: 'app-rating' })
export class RatingComponent {
@Input() rating = 0;
@Output() ratingChange = new EventEmitter<number>();
setRating(value: number) {
this.rating = value;
this.ratingChange.emit(value);
}
}
<!-- parent template -->
<app-rating [(rating)]="userRating" />
[(rating)] desugars to [rating]="userRating" (ratingChange)="userRating = $event".
Option B — model() (Angular 17.2+)
model() replaces the @Input() + @Output() pair with a single writable signal. The child updates it with .set(), and Angular syncs the new value back to the parent automatically.
// child component
import { model } from '@angular/core';
@Component({ selector: 'app-rating' })
export class RatingComponent {
rating = model(0); // writable signal, two-way bindable
setRating(value: number) {
this.rating.set(value); // notifies the parent automatically
}
}
<!-- parent template — identical syntax to Option A -->
<app-rating [(rating)]="userRating" />
The parent's userRating updates whenever the child calls this.rating.set(). Unlike input(), model() is writable — .set() and .update() are available inside the child.
16. Form Submission — ngSubmit
FormsModule enhances <form> elements. Bind (ngSubmit) to handle submission without a page reload.
Child component:
import { Component, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
...
})
export class CreateUserComponent {
title = '';
name = '';
email = '';
createUser = output<{ title: string; name: string; email: string }>();
onSubmit() {
this.createUser.emit({
title: this.title,
name: this.name,
email: this.email,
});
}
}
Child template:
<form (ngSubmit)="onSubmit()">
<input [(ngModel)]="title" name="title" />
<input [(ngModel)]="name" name="name" />
<input [(ngModel)]="email" name="email" />
<button type="submit">Create</button>
</form>
Parent template:
<app-create-user (createUser)="onCreate($event)" />
17. Content Projection — <ng-content>
Similar to children in React. Place <ng-content /> in a child component's template as a slot; whatever is placed between the child's opening and closing tags in the parent gets projected into that slot.
Card component template:
<div class="card">
<h2>Card Title</h2>
<ng-content />
</div>
Parent template:
<app-card>
<p>This paragraph is projected into the card.</p>
</app-card>
Multiple named slots — use the select attribute to target specific elements by CSS selector:
<div class="card">
<div class="card-header">
<ng-content select="[card-header]" />
</div>
<div class="card-body">
<ng-content select="[card-body]" />
</div>
</div>
<app-card>
<h2 card-header>My Title</h2>
<p card-body>My content goes here.</p>
</app-card>
18. Services & Dependency Injection
A service decorated with @Injectable({ providedIn: 'root' }) is instantiated once and shared across the entire app (singleton). Angular's DI system injects the same instance wherever it is requested.
Service:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TasksService {
private tasks = ['Task 1', 'Task 2'];
getTasks() {
return this.tasks;
}
}
Constructor injection (traditional style):
export class TasksComponent {
constructor(private taskService: TasksService) {}
getTasks() {
return this.taskService.getTasks();
}
}
inject() function (Angular 14+, preferred in modern Angular):
import { inject } from '@angular/core';
export class TasksComponent {
private taskService = inject(TasksService);
getTasks() {
return this.taskService.getTasks();
}
}
inject() is preferred in modern code because it works in field initializers (no constructor needed), plays well with signals, and can be used in standalone functions (e.g. route guards, interceptors) where a constructor is not available.
Component-scoped service — provide a service in the component's providers array to get a fresh instance per component (not a singleton):
@Component({
providers: [TasksService],
...
})
export class TasksComponent { ... }
19. Pipes — Transforming Template Output
Pipes transform a value directly in the template using the | operator. They do not mutate the original value.
<p>{{ username | uppercase }}</p>
<p>{{ dueDate | date:'mediumDate' }}</p>
<p>{{ price | currency:'EUR' }}</p>
Passing parameters — add :param after the pipe name. Chain multiple params with more colons:
<p>{{ 3.14159 | number:'1.0-2' }}</p> <!-- "3.14" -->
<p>{{ dueDate | date:'yyyy-MM-dd' }}</p>
Chaining pipes — apply multiple pipes left to right:
<p>{{ username | slice:0:10 | uppercase }}</p>
Common built-in pipes (all from @angular/common):
| Pipe | Example | Output |
|---|---|---|
uppercase / lowercase
|
`'hello' \ | uppercase` |
titlecase |
`'hello world' \ | titlecase` |
date |
`date \ | date:'shortDate'` |
currency |
`9.9 \ | currency:'USD'` |
number |
`3.14159 \ | number:'1.0-2'` |
percent |
`0.85 \ | percent` |
slice |
`'Angular' \ | slice:0:3` |
json |
`obj \ | json` |
async |
`obs$ \ | async` |
Importing pipes in standalone components — import each pipe individually (Angular 15+) rather than the entire CommonModule:
import { DatePipe, UpperCasePipe, CurrencyPipe } from '@angular/common';
@Component({
imports: [DatePipe, UpperCasePipe, CurrencyPipe],
...
})
export class MyComponent { ... }
Custom pipe — implement PipeTransform and decorate with @Pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 20): string {
return value.length > limit ? value.slice(0, limit) + '…' : value;
}
}
<p>{{ longText | truncate:50 }}</p>
Top comments (0)