View State Selector - Angular design pattern
In this article we are going to consider View State Selector - Angular design pattern.

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader).
<div *ngIf="data$ | async as data">
<ng-container *ngIf="data && !error">
...
</ng-container>
<ng-container *ngIf="error && !loading">
...
</ng-container>
<ng-container *ngIf="loading">
Loading...
</ng-container>
</div>
View State Selector is a pattern binding the component state with the corresponding view template. In other words, it automatically injects a view template for a specific component state. For example, a component that depends on a data retrieved by an HTTP request will start with a loading state that results in injecting the loader, then depending on the resolved state (error or success) it will switch to main or the error view.
In this short article I am going to share my View State Selector Pattern. Although this pattern can be generalized to choose one of any mutually exclusive views based only on any input state, in the next parts we will concentrate on the most common use case of changing the view according to the asynchronous HTTP state. So let’s dive in.
Pattern usage
The usage of “View State Selector” pattern is somewhat similar to an ngSwitchCase. Given an input representation of the state render the template matching the provided state:
<div *viewContainer="view$ | async;
main mainTmp;
error errorTmp;
loading loaderTmp">
</div>
<ng-template #mainTmp let-v="view">...</ng-template>
<ng-template #errorTmp let-v="view">...</ng-template>
<ng-template #loaderTmp>...</ng-template>
There are few scenarios this pattern will have advantage over using ngSwitchCase:
- The pattern can be utilized to reduce the boilerplate code of the conditions that determine the active template (view). These conditions are encapsulated into the viewContainer.
- For more complex states or when more than a single state selected at the same time. One usage example is implementing a skeleton loader. This can be done by using both the loading state and main view (data) state simultaneously.
- In general this pattern shines with more complicated states that can be represented by a state machine. In scenarios where simply using ngSwitch is possible but will require extra code to convert the state to enum for the ngSwitch.
Pattern Implementation
The View
in "View State Selector"
To achieve the goal of a reusable pattern we need to start by defining an interface to store the View
states. In general, a View
is an object representing the states of, well, the view. It can have as many states as your component needs. In this article I am going to focus on the three most used states:
Loading - the state before the asynchronous request has been resolved. This state will inject the Loader
template.
Data - upon a (successful) response the display data will be mapped into the main
template.
Error - if the request failed the error state will contain the reason for the failure and instructions for the error template display.
export class View<T> {
data?: T; // Store view data of type T
loader?: boolean;
error?: Error;
}
Next, we can map the state into the View
above. The view will startWith
emitting the loading state, then upon a successful response event (with the data T) we will map
it to View<T>
. And in case of an error we will add catchError
to map it into the error state.
const view$: Observable<View<T>> =
this.httpClient<T>(<url>).pipe(
startWith({loader: true}),
map(response => ({data: response})),
catchError(error => of({error})));
Note: T is a placeholder for the response type
The viewContainer
At this point we have an observable that will emit View
s. Now, we can create a structural directive (or component) to resolve the View
(state) and inject the corresponding view template.
In other words the ViewContainer
is responsible for injecting the corresponding template for a given view state.
<div *viewContainer="view$ | async;
main mainTmp;
error errorTmp;
loading loaderTmp">
<div>
<ng-template #mainTmp>...</ng-template>
<ng-template #errorTmp>...</ng-template>
<ng-template #loaderTmp>...</ng-template>
<view-container
*ngIf="view$ | async as view"
[appViewMain]="mainTmp"
[errorTmp]="errorTmp"
[loaderTmp]="loaderTmp"
[view]="view">
</view-container>
<ng-template #mainTmp>...</ng-template>
<ng-template #errorTmp>...</ng-template>
<ng-template #loaderTmp>...</ng-template>
ViewContainer
Directive implementation
The implementation for both the Directive and the Component are very similar, I will focus on the Directive implementation.
First, create an empty Directive
@Directive({ selector: '[viewContainer]' })
export class ViewContainerDirective<T> implements AfterViewInit {
ngAfterViewInit(): void {
// Verify all the templates defined, throw an error otherwise
}
}
Next, define the properties to save the reference templates:
private _mainTemplateRef: TemplateRef<AppViewContext<T>> = null;
private _errorTemplateRef: TemplateRef<AppViewContext<T>> = null;
private _loaderTemplateRef: TemplateRef<AppViewContext<T>> = null;
And to bind the template reference (#<name>) to the properties add:
@Input() set viewContainerMain(templateRef: TemplateRef<any>) {
this._mainTemplateRef = templateRef;
}
@Input() set viewContainerError(templateRef: TemplateRef<any>) {
this._errorTemplateRef = templateRef;
}
@Input() set viewContainerLoading(templateRef: TemplateRef<any>) {
this._loaderTemplateRef = templateRef;
}
In case you wonder how that binding works check the microsyntax for directives. In short, the setter name is a combination of the directive name (prefix) with the attribute name (suffix).
In the ngAfterViewInit
we will check that all the templates exist. If one of the templates is missing we will get an error that’s hard to miss.
ngAfterViewInit(): void {
if (!this._errorTemplateRef) throw new Error('Missing Error Template')
if (!this._loaderTemplateRef) throw new Error('Missing Loader Template')
if (!this._mainTemplateRef) throw new Error('Missing Main Template')
}
Finally, each time the View is changed insert the template to the container. For that we can use createEmbeddedView API So let's inject the ViewContainerRef Service.
constructor(private _viewContainer: ViewContainerRef) { }
One of the createEmbeddedView
optional parameters is a context. Providing the context will allow accessing the data (T - the one from the View<T>
).
private _context: AppViewContext<T> = new AppViewContext<T>();
Now, we have everything we need to implement the setter:
@Input() set viewContainer(view: View<T>) {
if (!view) return;
this._context.$implicit = view; // expose the view object to the template
this._viewContainer.clear(); // Clears the old template
if (view.loader)
this._viewContainer.createEmbeddedView(this._loaderTemplateRef, this._context);
if (view.error && !view.loader) // Defines the conditions to display each template in single place
this._viewContainer.createEmbeddedView(this._errorTemplateRef, this._context);
if (view.data && !view.error)
this._viewContainer.createEmbeddedView(this._mainTemplateRef, this._context);
}
Wrapping Up
In this article we implemented the "View State Selector" allowing us to simplify our components by reducing boilerplate code, flattening the templates and notifying us for missing templates. While at the same time reducing the chances for potential bugs by getting some feedback when something is missing.
You can find more examples and full implementation in this Github Repository.