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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Collapsing several observables into one view model stream means the template subscribes once instead of juggling multiple async pipes.
  2. 2A typed view model interface keeps the shape the template consumes explicit and refactor-safe.
  3. 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.

The view model pattern in Angular — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code