View State Selector  - Angular design pattern

Post Editor

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

5 min read
0 comments
post

View State Selector  - Angular design pattern

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

post
post
5 min read
0 comments
0 comments

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).

<>Copy
<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>
This is one example taken from a project I worked on, and it can appear in other permutations. It has a main view container, some conditions to determine the state, a loader template and error handling block.

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.

Content imageContent image

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
Link to this section

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:

<>Copy
<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>
In the example above the state represented by the view$ observable with three states: main, error and loading. Each state is bound to a template that will be rendered according to the view$ value.

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
Link to this section

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.

<>Copy
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.

<>Copy
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 Views. 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.

<>Copy
<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 State Selector as a structural directive
<>Copy
<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

<>Copy
@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:

<>Copy
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:

<>Copy
@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.

<>Copy
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') }
no more missing loader or error handlers!

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.

<>Copy
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>).

<>Copy
private _context: AppViewContext<T> = new AppViewContext<T>();

Now, we have everything we need to implement the setter:

<>Copy
@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.



Comments (0)

Be the first to leave a comment

Share

About the author

author_image

I'm a challenge driven software engineer and clean code advocate. Passionate about software design and architecture, focusing on web-dev and Angular.

author_image

About the author

Natan Braslavski

I'm a challenge driven software engineer and clean code advocate. Passionate about software design and architecture, focusing on web-dev and Angular.

About the author

author_image

I'm a challenge driven software engineer and clean code advocate. Passionate about software design and architecture, focusing on web-dev and Angular.

Featured articles