20 slides · Real code examples throughout · Angular v17+ focus
Angular is a full-featured, opinionated TypeScript framework maintained by Google for building single-page applications and progressive web apps.
| Year | Milestone |
|---|---|
| 2010 | AngularJS (v1.x) released by Google |
| 2016 | Angular 2 — complete rewrite in TypeScript |
| 2017-2022 | Angular 4-14 — Ivy renderer, strict mode |
| 2023 | Angular 16-17 — signals, new control flow, esbuild |
| 2024 | Angular 18-19 — zoneless, stable signals |
Angular is not AngularJS. The modern framework (v2+) is a complete rewrite with a fundamentally different architecture.
Organise related code into cohesive blocks. Standalone components are replacing NgModules in modern Angular.
Building blocks of the UI. Each has a TypeScript class, an HTML template, and optional CSS styles.
Encapsulate business logic and data access. Injected via Angular's hierarchical injector system.
The @angular/cli is the official tool for creating, developing, scaffolding, and building Angular projects.
# Install the CLI globally
npm install -g @angular/cli
# Create a new project
ng new my-app --style=scss --routing
# Serve with live reload
ng serve --open
# Generate artifacts
ng generate component features/dashboard
ng generate service core/auth
ng generate pipe shared/truncate
ng generate guard auth/role
| Command | Purpose |
|---|---|
ng new | Scaffold a full project |
ng serve | Dev server with HMR |
ng generate | Create components, services, etc. |
ng build | Production build (AOT + tree-shake) |
ng test | Run unit tests (Karma/Jest) |
ng lint | Lint with ESLint |
ng update | Update Angular + run migrations |
ng add | Add libraries with schematics |
import { Component } from '@angular/core';
@Component({
selector: 'app-hero-card',
standalone: true,
template: `
<div class="hero-card">
<h2>{{ hero.name }}</h2>
<p>Power: {{ hero.power }}</p>
<button (click)="onSelect()">
Select
</button>
</div>
`,
styles: [`
.hero-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
}
`]
})
export class HeroCardComponent {
hero = { name: 'Windstorm', power: 'Weather' };
onSelect() {
console.log(`Selected: ${this.hero.name}`);
}
}
selector — custom HTML tag nametemplate / templateUrl — inline or external HTMLstyles / styleUrls — scoped CSSstandalone — no NgModule needed{{ expr }} — interpolation[prop]="expr" — property binding(event)="handler()" — event binding[(ngModel)]="val" — two-way bindingngOnInit — after first data bindingngOnChanges — when inputs changengOnDestroy — cleanup subscriptionsAngular provides four forms of data binding that connect the component class to the template.
<h1>Welcome, {{ user.name }}!</h1>
<p>Total: {{ getTotal() | currency }}</p>
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="isLoading">Submit</button>
<div [class.active]="isActive"></div>
<button (click)="save()">Save</button>
<input (keyup.enter)="search(term)">
<div (mouseover)="highlight($event)"></div>
<!-- Requires FormsModule -->
<input [(ngModel)]="username">
<!-- Desugared equivalent -->
<input [ngModel]="username"
(ngModelChange)="username = $event">
| Syntax | Direction | Example |
|---|---|---|
{{ }} | Component → DOM | Text interpolation |
[ ] | Component → DOM | Property / attribute / class / style |
( ) | DOM → Component | User events (click, keyup, etc.) |
[( )] | Both | Form inputs with ngModel |
<!-- *ngIf -->
<div *ngIf="user; else noUser">
Hello, {{ user.name }}
</div>
<ng-template #noUser>
<p>Please log in</p>
</ng-template>
<!-- *ngFor -->
<li *ngFor="let item of items;
trackBy: trackById; let i = index">
{{ i + 1 }}. {{ item.name }}
</li>
<!-- *ngSwitch -->
<div [ngSwitch]="role">
<p *ngSwitchCase="'admin'">Admin</p>
<p *ngSwitchDefault>User</p>
</div>
<!-- @if / @else -->
@if (user) {
<p>Hello, {{ user.name }}</p>
} @else {
<p>Please log in</p>
}
<!-- @for with required track -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>No items found</li>
}
<!-- @switch -->
@switch (role) {
@case ('admin') { <p>Admin</p> }
@default { <p>User</p> }
}
ngClass — conditional CSS classesngStyle — conditional inline styles@Directive({ selector: '[appHighlight]' })Angular Signals (stable in v17+) provide fine-grained, synchronous reactivity without Zone.js.
import {
signal, computed, effect
} from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed (read-only, auto-tracked)
double = computed(() => this.count() * 2);
constructor() {
// Side-effect that re-runs on change
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
}
Creates a writable reactive value. Read by calling it as a function. Mutate with .set(), .update(), or .mutate().
Derives a read-only signal from other signals. Lazily evaluated and memoized — only recalculates when dependencies change.
Runs a side-effect whenever tracked signals change. Useful for logging, localStorage sync, or analytics.
toSignal() / toObservable() for interopimport { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class HeroService {
private http = inject(HttpClient);
private apiUrl = '/api/heroes';
getAll() {
return this.http.get<Hero[]>(this.apiUrl);
}
getById(id: number) {
return this.http.get<Hero>(
`${this.apiUrl}/${id}`
);
}
create(hero: Partial<Hero>) {
return this.http.post<Hero>(
this.apiUrl, hero
);
}
}
// Usage in a component
@Component({ /* ... */ })
export class HeroListComponent {
private heroService = inject(HeroService);
heroes = signal<Hero[]>([]);
ngOnInit() {
this.heroService.getAll().subscribe(
data => this.heroes.set(data)
);
}
}
providedIn: 'root' registers a singleton at the root injector. Tree-shakeable — only included if actually injected.
Modern alternative to constructor injection. Can be used in components, directives, pipes, and services.
export const API_URL =
new InjectionToken<string>('API_URL');
// Provide it
{ provide: API_URL, useValue: '/api' }
// Inject it
url = inject(API_URL);
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Subject, switchMap, debounceTime,
distinctUntilChanged, catchError,
of } from 'rxjs';
import { HeroService } from './hero.service';
@Component({
selector: 'app-search',
standalone: true,
imports: [AsyncPipe],
template: `
<input (input)="onSearch($event)">
@for (hero of results$ | async;
track hero.id) {
<p>{{ hero.name }}</p>
}
`
})
export class SearchComponent {
private search$ = new Subject<string>();
private heroService = inject(HeroService);
results$ = this.search$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
this.heroService.search(term).pipe(
catchError(() => of([]))
)
)
);
onSearch(e: Event) {
const val = (e.target as HTMLInputElement).value;
this.search$.next(val);
}
}
| Operator | Purpose |
|---|---|
map | Transform emitted values |
filter | Emit only matching values |
switchMap | Cancel previous inner observable |
mergeMap | Run inner observables concurrently |
combineLatest | Combine latest from multiple streams |
takeUntilDestroyed | Auto-unsubscribe on destroy |
Subscribes in the template and auto-unsubscribes. Preferred over manual .subscribe() for cleaner code.
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{
path: 'heroes',
loadComponent: () =>
import('./heroes/list.component')
.then(m => m.HeroListComponent),
},
{
path: 'heroes/:id',
loadComponent: () =>
import('./heroes/detail.component')
.then(m => m.HeroDetailComponent),
resolve: { hero: heroResolver },
canActivate: [authGuard],
},
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canMatch: [adminGuard],
},
{ path: '**', component: NotFoundComponent },
];
loadComponent and loadChildren split code into separate chunks — loaded on demand for faster initial load.
export const authGuard: CanActivateFn =
(route, state) => {
const auth = inject(AuthService);
return auth.isLoggedIn()
? true
: inject(Router)
.createUrlTree(['/login']);
};
export const heroResolver: ResolveFn<Hero> =
(route) => {
const id = +route.paramMap.get('id')!;
return inject(HeroService).getById(id);
};
Angular offers two approaches: Template-driven (simple, directive-based) and Reactive (explicit, code-based).
<form #f="ngForm" (ngSubmit)="save(f)">
<input name="name"
ngModel required
minlength="3"
#name="ngModel">
@if (name.invalid && name.touched) {
<span class="error">
Name is required (min 3 chars)
</span>
}
<button [disabled]="f.invalid">
Save
</button>
</form>
Uses FormsModule. Good for simple forms with minimal validation.
@Component({ /* ... */ })
export class ProfileFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [
Validators.required,
Validators.minLength(3)
]],
email: ['', [
Validators.required,
Validators.email
]],
address: this.fb.group({
street: [''],
city: [''],
zip: ['', Validators.pattern(/\d{5}/)]
})
});
save() {
if (this.form.valid) {
console.log(this.form.getRawValue());
}
}
}
Uses ReactiveFormsModule. Typed, testable, dynamic.
// app.config.ts — provide HttpClient
import { provideHttpClient, withInterceptors }
from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
),
]
};
// auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
const token = inject(AuthService).getToken();
if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next(req).pipe(
catchError(err => {
if (err.status === 401) {
inject(Router).navigate(['/login']);
}
return throwError(() => err);
})
);
};
Standalone replacement for HttpClientModule. Supports functional interceptors and fetch backend.
interface Hero {
id: number;
name: string;
power: string;
}
this.http.get<Hero[]>('/api/heroes')
.subscribe(heroes => {
// heroes is Hero[]
});
catchError — handle per-request errorsretry(3) — automatic retriesHttpErrorResponse — typed error objectPipes transform displayed values in templates. Angular ships with many built-in pipes and supports custom pipes.
| Pipe | Example Output |
|---|---|
date | {{ d | date:'mediumDate' }} → Apr 6, 2026 |
currency | {{ 42.5 | currency:'EUR' }} → €42.50 |
uppercase | {{ 'hello' | uppercase }} → HELLO |
json | Debug-prints object as JSON |
async | Subscribes to Observable/Promise |
slice | Subset of an array or string |
keyvalue | Iterates over object entries |
import { Pipe, PipeTransform } from
'@angular/core';
@Pipe({
name: 'truncate',
standalone: true,
pure: true // default
})
export class TruncatePipe
implements PipeTransform {
transform(
value: string,
limit = 50,
ellipsis = '...'
): string {
if (!value) return '';
return value.length > limit
? value.substring(0, limit) + ellipsis
: value;
}
}
// Usage in template
// {{ longText | truncate:30:'…' }}
Only re-evaluate when input reference changes. Default and performant.
Re-evaluate on every change detection cycle. Use sparingly — pure: false.
// child.component.ts
@Component({
selector: 'app-child',
template: `
<p>{{ title }}</p>
<button (click)="notify.emit('hi')">
Notify Parent
</button>
`
})
export class ChildComponent {
@Input({ required: true }) title!: string;
@Output() notify = new EventEmitter<string>();
}
// parent template
<app-child
[title]="parentTitle"
(notify)="onNotify($event)">
</app-child>
@Component({
selector: 'app-card',
template: `
<h2>{{ title() }}</h2>
<p>{{ uppercaseTitle() }}</p>
`
})
export class CardComponent {
title = input.required<string>();
subtitle = input('Default value');
uppercaseTitle = computed(
() => this.title().toUpperCase()
);
}
@ViewChild('chart') chartRef!: ElementRef;
@ViewChild(ChildComponent) child!: ChildComponent;
@ContentChild(HeaderDirective) header!: HeaderDirective;
<!-- card.component.html -->
<div class="card">
<ng-content select="[header]"></ng-content>
<ng-content></ng-content>
<ng-content select="[footer]"></ng-content>
</div>
<!-- Usage -->
<app-card>
<h2 header>Title</h2>
<p>Body content here</p>
<button footer>Action</button>
</app-card>
Since Angular 14+, components can be standalone — no NgModule required. This is now the recommended default in Angular 17+.
// standalone component
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
RouterLink,
HeroCardComponent,
TruncatePipe
],
template: `
@for (hero of heroes(); track hero.id) {
<app-hero-card [hero]="hero" />
}
<a routerLink="/settings">Settings</a>
`
})
export class DashboardComponent {
heroes = signal<Hero[]>([]);
}
// bootstrap without NgModule
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor])
),
provideAnimationsAsync(),
]
});
loadComponentng g @angular/core:standalonestandalone: true to componentsimports arrayprovide* functions// Bridge for libraries still using NgModule
providers: [
importProvidersFrom(
SomeLibraryModule.forRoot()
)
]
import { ComponentFixture, TestBed }
from '@angular/core/testing';
describe('HeroCardComponent', () => {
let component: HeroCardComponent;
let fixture: ComponentFixture<
HeroCardComponent
>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HeroCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(
HeroCardComponent
);
component = fixture.componentInstance;
});
it('should display hero name', () => {
component.hero =
{ id: 1, name: 'Storm', power: 'Weather' };
fixture.detectChanges();
const el: HTMLElement =
fixture.nativeElement;
expect(el.querySelector('h2')?.textContent)
.toContain('Storm');
});
it('should emit on select', () => {
spyOn(component.notify, 'emit');
const btn = fixture.nativeElement
.querySelector('button');
btn.click();
expect(component.notify.emit)
.toHaveBeenCalled();
});
});
describe('HeroService', () => {
let service: HeroService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(HeroService);
httpMock = TestBed.inject(
HttpTestingController
);
});
it('should fetch heroes', () => {
service.getAll().subscribe(heroes => {
expect(heroes.length).toBe(2);
});
const req = httpMock.expectOne('/api/heroes');
req.flush([{ id: 1 }, { id: 2 }]);
});
});
| Tool | Role |
|---|---|
| Jasmine | Test framework (default) |
| Karma | Test runner (legacy) |
| Jest | Modern alternative (v16+) |
| Web Test Runner | Official replacement for Karma |
| Cypress / Playwright | E2E testing |
providedIn: 'root' enables tree-shaking for servicesloadComponent / loadChildren// angular.json — new builder
{
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputMode": "static",
"ssr": false
}
}
# Add SSR to existing project
ng add @angular/ssr
# Generates:
# - server.ts (Express server)
# - app.config.server.ts
# - Build outputs browser + server bundles
Thank you! — Built with Reveal.js · Single self-contained HTML file