Create a global error component for Angular 4

I quite like the material design in Angular (using @angular/material 2.0.0-beta.12). One component I’m missing is an error component which is global for an entire form (or any other kind of user interaction). Some errors cannot be tied to a specific input field, such errors should be displayed on the form but independent of any individual input control. (eg. “Data on this form has been changed in the meantime. Submit again to overwrite, or refresh“, or “A customer with identical name has already been registered for the same address.“)

In this post I’ll show how we’ve implemented a custom Angular component to that purpose.

formerror-component

Given: a global error handler

In our CoreModule, we’ve defined a

providers: [ { provide: ErrorHandler, useClass: GlobalErrorHandler } ]

The GlobalErrorHandler extends ErrorHandler, and stores an error context .

import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { LocationStrategy, PathLocationStrategy } from '@angular/common';

import { Subject } from 'rxjs/Subject';

import { ErrorContext } from './errorcontext.interface';

@Injectable()
export class GlobalErrorHandler extends ErrorHandler {

  private emptyErrorContext: ErrorContext = {
   message: '',
   location: ''
  };

  private errorContextSubject = new Subject<ErrorContext>();
  errorContext$ = this.errorContextSubject.asObservable();

  /**
   * Since error handling is really important it needs to be loaded first,
   * thus making it not possible to use dependency injection in the constructor
   * to get other services such as the error handle API service to send the server
   * our error details
   * @param injector
   */
  constructor( private injector: Injector ) {
    super( false );
    this.errorContextSubject.next( this.emptyErrorContext );
  }

  handleError( error ) {
    const locationStrategy = this.injector.get( LocationStrategy );
    let nextErrorContext: ErrorContext = {
      message: error.message ? error.message : error.toString(),
      location: locationStrategy instanceof PathLocationStrategy ? locationStrategy.path() : ''
    };
    this.errorContextSubject.next( nextErrorContext );
    super.handleError( error );
  }
}

The reason we’re using ‘extends’ instead of ‘implements’ is that it allows us to pass on the error to super, which eventually calls the Error.prototype.handleError().

Some notes where we got sidetracked:

  • Compiler warning about exports not being found are discussed here, the solution was to separate the ErrorContext interface into a separate file
  • Some stackoverflow posts state “When applying this on the root module, all children modules will receive the same error handler (unless they have another one specified in their provider list).“, which is misleading. Providing the GlobalErrorHandler on the CoreModule exposes it to sibling modules, not just children.

 

The error component

The global form error HTML (formerror.component.html) is pretty straight-forward

The component hooks up with the GlobalErrorHandler described above, and subscribes to changes in the error context.

@Component( {
 selector: 'mymat-form-error',
 templateUrl: './formerror.component.html'
} )
export class FormerrorComponent implements OnInit, OnDestroy {

  errorContextState$: Observable<ErrorContext>;
  errorContext: ErrorContext;
  private errorContextSubscription: Subscription;

  constructor( private errorHandler: ErrorHandler, private cdRef: ChangeDetectorRef ) {
    const defaultError: ErrorContext = {
      message: '',
      location: ''
    };
    this.errorContext = defaultError;
    if ( this.errorHandler instanceof GlobalErrorHandler ) {
      this.errorContextState$ = this.errorHandler.errorContext$;
    }
  }

  ngOnInit() {
    if ( this.errorContextState$ ) {
      this.errorContextSubscription = this.errorContextState$
      .subscribe( data => {
        this.errorContext = data;
        this.cdRef.detectChanges();
      } );
    }
  }

  ngOnDestroy() {
    if ( this.errorContextSubscription ) {
      this.errorContextSubscription.unsubscribe();
    }
  }
}

There were two main issues:

Injecting an ErrorHandler

Trying to inject the custom GlobalErrorHandler instead of the base class resulted in a

Error: Uncaught (in promise): Error: No provider for GlobalErrorHandler!

I suspect that at the time of injection analysis, the error handler isn’t yet instantiated.  Debugging never shows a c’tor with anything other than the GlobalErrorHandler, so the error must occur before actual component instantiation

Component UI update

Without the call to ChangeDetectorRef#detectChanges(), the UI was not updated when eg. an HTTP POST resulted in an error. Whenever Angular (zone) detected a change and eventually updated the UI, the error component showed the previous error. I don’t yet understand why change detection doesn’t get this instance, but there seem to be rather a few similar issues out there (eg. this one in combination with redux)

 

 


In the process of developing funnel.travel, a corporate post-booking travel management tool, I’m sharing some hopefully useful insights into Angular 4, Spring Boot, jOOQ, or any other technology we’ll be using.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s