DEV Community

loading...

View Pattern - Angular design pattern

natanbr profile image Natan Braslavski Originally published at indepth.dev Updated on ・4 min read

View Pattern is a front-end design pattern. The View Pattern is a way to automatically inject a view corresponding to the asynchronous request state. For example, a component that depends on a data retrieved by an HTTP request will start with a loading state then depends on the resolved state (error or success) it will switch to main or the error view.

If you are a web front-end developer you may recognize a repetitively UI pattern of displaying a loader while an asynchronous request is being processed, then switching do the main view or displaying and error. Personally, I noticed that in single page applications with multiple components per page that being loaded independently I have a repetitive code. And even worst, is the fact that I am not getting any indication for missing templates (if I forgot to implement an error handler or a loader).

In this short article I am going to share my "View Pattern" solution attempting to reduce code duplication and notify for missing parts.

The View in "View Pattern"

To achieve the goal of a reusable view pattern we need to start by defining an interface to store the View state. This view state can be of any complexity, but for this article I will focused the following 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 component data (of generic type T)
  loader?: boolean; // Whether to show the loader
  error?: Error;
}
Enter fullscreen mode Exit fullscreen mode

In this specific implementation I am going to use RxJS Observables to handle the async events and manipulate the flow.
For every event coming out our main event emitter we will wrap it in a View state. Since http is the most used observable we will use it for our example: const view$: Observable<View<T>> = this.httpClient<T>(<url>).
We will startWith emitting the loading state. Then when we receive the response event (with the data T) we will map it to View<T>. For handling errors we will add catchError.

const request$: Observable<View<T>> = this.httpClient<T>(<url>).pipe(
  startWith({loader: true}),
  map(response => ({data: response})),
  catchError(error => of({error})));
Enter fullscreen mode Exit fullscreen mode

Note:

  • T is a placeholder for the response type

"View Pattern" viewContainer

The ViewContainer is responsible to inject the correct template for a given view. In this tutorial we will use a structural directive as an example.

The usage will look as follows:

<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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

In the next part we are going to implement that structural directive. But, it could also be a component. If you are interested you can find full implementations.

viewContainer Implementation

First let's create our Directive

@Directive({ selector: '[viewContainer]' })
export class ViewContainerDirective<T> implements AfterViewInit {

   ngAfterViewInit(): void {
       // Verify all the templates defined, throw an error otherwise 
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

and bind the template reference (#<name>) to the properties.

@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;
}

Enter fullscreen mode Exit fullscreen mode

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

Now, let's go back to the ngAfterViewInit and add the check if one of the templates is missing

  ngAfterViewInit(): void {
    if (!this._errorTemplateRef) throw new Error('View Pattern: Missing Error Template')
    if (!this._loaderTemplateRef) throw new Error('View Pattern: Missing Loader Template')
    if (!this._mainTemplateRef) throw new Error('View Pattern: Missing Main Template')
  }
Enter fullscreen mode Exit fullscreen mode

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) { }
Enter fullscreen mode Exit fullscreen mode

One of 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>();
Enter fullscreen mode Exit fullscreen mode

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

@Input() set viewContainer(view: View<T>) {
    if (!view) return;

    this._context.$implicit = view; // setting view to be avilable in the template
    this._viewContainer.clear(); // Clears the old template before setting the the new one.

    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);
  }
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

In this tutorial we implemented the "view pattern" allowing us to simplify our components by reducing duplicated code, flattening the templates. While at the same time reducing the chances for potential bugs by getting some feedback when something is missing.
That pattern can be easily extended to support more complicated states, and it will support "skeleton loaders" out of the box by providing both the mock data and the loader. You can check the full code and examples on Github.

Discussion (0)

pic
Editor guide