typescript
54 lines · 6 steps
The view model pattern in Angular
One header component merges three reactive stores into a single view model stream the template renders with the async pipe.
Explained by
highlit
1import { Component, inject } from '@angular/core';
2import { combineLatest, map, startWith } from 'rxjs';
3import { UserStore } from './user.store';
4import { CartStore } from './cart.store';
5import { NotificationStore } from './notification.store';
6
7interface HeaderViewModel {
8 displayName: string;
9 avatarUrl: string;
10 cartCount: number;
11 unreadCount: number;
12 isPremium: boolean;
13}
14
15@Component({
16 selector: 'app-header',
17 standalone: true,
18 template: `
19 <header *ngIf="vm$ | async as vm" class="app-header">
20 <img [src]="vm.avatarUrl" [alt]="vm.displayName" class="avatar" />
21 <span class="greeting">Hi, {{ vm.displayName }}</span>
22 <span *ngIf="vm.isPremium" class="badge">PRO</span>
23
24 <a routerLink="/notifications" class="icon">
25 <mat-icon>notifications</mat-icon>
26 <span *ngIf="vm.unreadCount" class="pill">{{ vm.unreadCount }}</span>
27 </a>
28
29 <a routerLink="/cart" class="icon">
30 <mat-icon>shopping_cart</mat-icon>
31 <span *ngIf="vm.cartCount" class="pill">{{ vm.cartCount }}</span>
32 </a>
33 </header>
34 `,
35})
36export class HeaderComponent {
37 private readonly users = inject(UserStore);
38 private readonly cart = inject(CartStore);
39 private readonly notifications = inject(NotificationStore);
40
41 readonly vm$ = combineLatest([
42 this.users.currentUser$,
43 this.cart.itemCount$,
44 this.notifications.unreadCount$.pipe(startWith(0)),
45 ]).pipe(
46 map(([user, cartCount, unreadCount]): HeaderViewModel => ({
47 displayName: user.fullName ?? user.email,
48 avatarUrl: user.avatarUrl ?? '/assets/default-avatar.png',
49 cartCount,
50 unreadCount,
51 isPremium: user.plan === 'premium',
52 })),
53 );
54}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Collapsing several observables into one view model stream means the template subscribes once instead of juggling multiple async pipes.
- 2A typed view model interface keeps the shape the template consumes explicit and refactor-safe.
- 3startWith gives a stream an immediate value so combineLatest can emit before every source has fired.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/the-view-model-pattern-in-angular-explained-typescript-fcc8/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.