A while ago I wrote about Smart components, UI components and sandbox facades in Angular. This article is about reactive ViewModels for UI components in Angular. We will learn how this approach will result in cleaner templates, more reactive code and how it will give us some extra possibilities regarding performance optimization.

What’s a ViewModel?

A ViewModel is something that has a lot of different definitions and meanings. There are lots of different opinions about what a ViewModel should represent and which responsibility it has. This article is not about starting a discussion and we will be clear about what it means for the context of this article. A reactive ViewModel is a reactive model that contains up-to-date data that is specific to the template of a component.

Update: We have updated the boilerplate and performance of ViewModels in this article: Reactive input state for angular ViewModels.

The pager

To illustrate the power behind a reactive ViewModel for a UI component, we need an example. For this article, we will create a reactive Angular pager component.

Why a pager?

  • It holds some complexity: It has to calculate some stuff based on the inputs
  • It’s standalone
  • It’s a UI component, meaning a dumb component. ViewModels can also be used in smart components but this article is about ViewModels in UI components.

Let’s list the inputs of this pager component:

/**
 * The amount of items a page should show
 */
@Input() public itemsPerPage: number;

/**
 * The total of items in the database
 */
@Input() public total: number;

/**
 * The index of the page (starts from 0)
 */
@Input() public pageIndex: number;

The template of our pager looks like this:

  Showing {{ itemFrom }} to {{ itemTo }} of {{ total }} entries
  <button (click)="goToStart()" [disabled]="previousDisabled">Begin</button>
  <button (click)="previous()" [disabled]="previousDisabled">Previous</button>
  <button (click)="next()" [disabled]="nextDisabled">Next</button>
  <button (click)="goToEnd()" [disabled]="nextDisabled">End</button>

We can see that we need 5 values in our template:

  • itemFrom
  • itemTo
  • total
  • previousDisabled
  • nextDisabled

The total property is the same as its input so we don’t have to calculate this one, but the other 4 values need to be calculated. We could calculate this in the template, but we want to put this in the class because:

  • We don’t want to repeat the logic for previousDisabled and nextDisabled.
  • We want to avoid logic in the templates because it is harder to unit test
  • Less complexity in the template results in better separation of concerns

Calculating this in the class means that we have to care about the order of when inputs get new values. The inputs: itemsPerPage, total and pageIndex can change all the time and when one of these changes, all the values (except total) need to be recalculated. We could do that in the ngOnChanges() lifecycle hook but that would result in brittle code and in this article we want to use a reactive approach.

Creating a reactive ViewModel

Update:

In Angular 17 where signals are out of developer preview everything could be simplified to this:

private readonly itemsPerPageSignal = signal(0);
private readonly totalSignal = signal(0);
private readonly pageIndexSignal = signal(0);

@Input() public set itemsPerPage(v: number) {
    this.itemsPerPageSignal.set(v);
};
@Input() public set total(v: number {
  this.totalSignal.set(v);
};
@Input() public set pageIndex(v: number {
  this.pageIndexSignal.set(v);
};

private readonly viewModel = computed(() => {
  return {
    total: this.totalSignal(),
    previousDisabled: this.pageIndexSignal() === 0,
    nextDisabled: this.pageIndexSignal() >= Math.ceil(this.totalSignal() / this.itemsPerPageSignal()) - 1,
    itemFrom: this.pageIndexSignal() * this.itemsPerPageSignal() + 1,
    itemTo:
            this.pageIndexSignal() < Math.ceil(this.totalSignal() / this.itemsPerPageSignal()) - 1
                    ? this.pageIndexSignal() * this.itemsPerPageSignal() + this.itemsPerPageSignal()
                    : this.totalSignal(),
  };
});
public get vm() {
    return this.viewModel();
}

If you are on Angular <16, the rest of this article will be for you!

We will use a combination of input setters and BehaviorSubjects to achieve this. A BehaviorSubject is a ReplaySubject that replays the last value and has an initial value. Why do we need an initial value? Because later on we will use combineLatest to create a reactive ViewModel object and this operator will not emit unless all its source observables have emitted (which is exactly what a BehaviorSubject does initially). We will suffix the subjects with $$ so we know they are not regular observables but subjects:

// Creating the BehaviorSubjects with an initial value
private readonly itemsPerPage$$ = new BehaviorSubject<number>(0);
private readonly total$$ = new BehaviorSubject<number>(0);
private readonly pageIndex$$ = new BehaviorSubject<number>(0);

Now let’s update the Inputs with setters that will feed the BehaviorSubjects we have just created:

/**
 * The amount of items a page should show
 */
@Input() public set itemsPerPage(v: number) {
    this.itemsPerPage$$.next(v);
};

/**
 * The total of items in the database
 */
@Input() public set total(v: number) {
    this.total$$.next(v);
};

/**
 * The index of the page (starts from 0)
 */
@Input() public set pageIndex(v: number) {
    this.pageIndex$$.next(v);
};

The next step is to create the ViewModel. We can start with the ViewModel type, which holds the properties that our template needs:

  • itemFrom
  • itemTo
  • total
  • previousDisabled
  • nextDisabled

We can add this type in the same file as our component since it won’t be used anywhere else:

type ViewModel = {
    itemFrom: number;
    itemTo: number;
    total: number;
    previousDisabled: boolean;
    nextDisabled: boolean;
}

This feels nice: a specific type just for our template. This means our template should not access anything else unless some public functions that our component class offers.

Now let’s create a vm$ observable that will get updated every time one of our inputs changes:

public readonly vm$: Observable<ViewModel> = combineLatest({
    itemsPerPage: this.itemsPerPage$$,
    total: this.total$$,
    pageIndex: this.pageIndex$$,
  }).pipe(
    map(({ itemsPerPage, total, pageIndex }) => {
      // we could extract this in a reusable function if
      // we want to.
      return {
        total,
        previousDisabled: pageIndex === 0,
        nextDisabled: pageIndex >= Math.ceil(total / itemsPerPage) - 1,
        itemFrom: pageIndex * itemsPerPage + 1,
        itemTo:
          pageIndex < Math.ceil(total / itemsPerPage) - 1
            ? pageIndex * itemsPerPage + itemsPerPage
            : total,
      };
    })
  );

Let’s not focus on the complexity inside the ViewModel, but it looks better here than it would be in the template, right?! Did we mention that you only need one async pipe for this, meaning only one subscription? We can use ng-container in combination with *ngIf to only subscribe to our ViewModel once and create a local template variable:

<ng-container *ngIf="vm$|async as vm">
    Showing {{ vm.itemFrom }}
    to {{ vm.itemTo }} of 
    {{ vm.total }} entries
    <button (click)="goToStart()" 
      [disabled]="vm.previousDisabled">
      Begin
    </button>
    <button (click)="previous()" 
      [disabled]="vm.previousDisabled">
      Previous
    </button>
    <button (click)="next()" 
      [disabled]="vm.nextDisabled">
      Next
    </button>
    <button (click)="goToEnd()" 
      [disabled]="vm.nextDisabled">
      End
    </button>
</ng-container>

Now we see that the vm is available everywhere as a template variable which makes it convenient to consume the values in the template. There is one more problem to solve. There is no interaction with the buttons yet. We need one Output and four methods to make our pager complete:

/**
 * Notifies the parent when the page has changed
 */
@Output() public readonly pageIndexChange = new EventEmitter<number>();

public goToStart(): void {
    this.pageIndexChange.emit(0);
}

public next(vm: ViewModel): void {
    this.pageIndexChange.emit(/*pageIndex*/ + 1);
}

public previous(vm: ViewModel): void {
    this.pageIndexChange.emit(/*pageIndex*/ - 1);
}

public goToEnd(vm: ViewModel): void {
    this.pageIndexChange.emit(Math.ceil(vm.total / /*itemsPerPage*/) - 1);
}

We can see here that we are missing 2 vital properties to communicate with the parent component:

  • pageIndex
  • itemsPerPage

Let’s add these to the ViewModel type and update the ViewModel like this:

type ViewModel = {
  itemFrom: number;
  itemTo: number;
  total: number;
  previousDisabled: boolean;
  nextDisabled: boolean;
  pageIndex: number;
  itemsPerPage: number;
};
...
public readonly vm$: Observable<ViewModel> = combineLatest(...)
  .pipe(
    map(({ itemsPerPage, total, pageIndex }) => {
      return {
        // add them here so they are available on the ViewModel
        itemsPerPage,
        pageIndex,
        ...
      };
    })
  );
...

This technique is powerful, since we have the vm as a template variable we have access to the latest value of the ViewModel everywhere. We can now pass the vm property to the next, previous and goToEnd functions where we need the pageIndex and itemsPerPage:

...
@Component({
  ...
  template: `
    ...
    <button (click)="previous(vm)" ...>
      Previous
    </button>
    <button (click)="next(vm)" ...>
      Next
    </button>
    <button (click)="goToEnd(vm)" ...>
      End
    </button>
    ...
  `,
})
export class PagerComponent {
  ...
  // pass the vm
  public next(vm: ViewModel): void {
    this.pageIndexChange.emit(vm.pageIndex + 1);
  }

  // pass the vm
  public previous(vm: ViewModel): void {
    this.pageIndexChange.emit(vm.pageIndex - 1);
  }

  // pass the vm
  public goToEnd(vm: ViewModel): void {
    this.pageIndexChange.emit(Math.ceil(vm.total / vm.itemsPerPage) - 1);
  }
}

We now have a fully working pager component that is reactive, testable and easy to read.

Optimizing for performance

The calculations are not that heavy but we could still optimize the ViewModel a bit more by leveraging the distinctUntilChanged operator. This operator will only emit new results when the new value is different than the previous value:

 public readonly vm$: Observable<ViewModel> = combineLatest({
    itemsPerPage: this.itemsPerPage$$.pipe(distinctUntilChanged()),
    total: this.total$$.pipe(distinctUntilChanged()),
    pageIndex: this.pageIndex$$.pipe(distinctUntilChanged()),
  }).pipe(
    ...
  );

For this example, the performance gain will be trivial, but it should prove the amount of control we have on how to optimize this for performance.

Demo

You can check out the full working component in this Stackblitz example

Conclusion

We have learned that introducing ViewModels on UI components can be done by using:

  • BehaviorSubjects
  • Setters
  • the combineLatest operator

Introducing these ViewModels results in:

  • Cleaner templates
  • Better testability
  • Less redundancy of complexity in templates
  • A way to optimize for performance, eg for Change Detection
  • A more reactive way of programming

If you liked the article, please leave a comment! Creating ViewModels could be done a lot cleaner by using ObservableState

If you like to learn directly from me, check out my Angular Training and Angular Coaching