5

Im using: Angular V6.1.0, Angular Material V6.4.1

Im trying catch the HTTP errors and show them using a MatSnackBar. I seek to show this in every component of my application (where there is an http request). So as not to do the repetitive code

Otherwise i should repeat the same code in every component for display the MatSnackBar with the errors inserted.

This is my service:

import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
// import { HttpClient, HttpErrorResponse, HttpRequest } from '@angular/common/http';
import { Observable, throwError, of, interval, Subject } from 'rxjs';
import { map, catchError, retryWhen, flatMap } from 'rxjs/operators';
import { url, ErrorNotification } from '../globals';
import { MatSnackBar } from '@angular/material';
import { ErrorNotificationComponent } from '../error-notification/error-notification.component';


@Injectable({
  providedIn: 'root'
})
export class XhrErrorHandlerService {
  public subj_notification: Subject<string> = new Subject();

  constructor(
    public snackBar: MatSnackBar
    ) {

  }

  public handleError (error: HttpErrorResponse | any) {
    this.snackBar.open('Error message: '+error.error.error, 'action', {
      duration: 4000,
    });
    return throwError(error);
  }
}
Simón Farias
  • 732
  • 2
  • 8
  • 21
  • So what's your issue exactly? – Jeto Nov 04 '18 at 23:11
  • The service can't display the MatSnackBar, i have a central service who catch every 4xx HTTP errors in every request and it show them. – Simón Farias Nov 04 '18 at 23:26
  • Are you getting an error? Are you sure your `handleError` method is executed? Can you show in what context it's being called? – Jeto Nov 04 '18 at 23:30
  • No, im not getting an error. Its a problem of architecture. Because naturally you cant display a MatSnackBar in a service, you must call it in a Component, obligatorily. Here is something similar to what happens to me: https://stackoverflow.com/questions/42761039/how-to-use-snackbar-on-service-to-use-in-every-component-in-angular-2 But it does not work (well, not for me). – Simón Farias Nov 05 '18 at 00:14
  • 1
    You can display a MatSnackBar from a service (though it may seem a bit suspicious), no reason you shouldn't. [Example StackBlitz](https://stackblitz.com/edit/angular-39vyy6?file=src%2Fapp%2Fsnackbar.service.ts). – Jeto Nov 05 '18 at 07:46

4 Answers4

6

Create a service with this:

custom-snackbar.service.ts

import { Injectable, NgZone } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';

@Injectable()
export class CustomSnackbarService {
    constructor(
      private snackBar: MatSnackBar,
      private zone: NgZone
    ) {
       
    }

    public open(message: string, action = 'success', duration = 4000): void {
        this.zone.run(() => {
          this.snackBar.open(message, action, { duration });
        });
    }
}

Add the MatSnackBarModule to the app.module.ts:

import { MatSnackBarModule } from '@angular/material/snack-bar';

...
imports: [
    BrowserModule,
    AppRoutingModule,
    MatSnackBarModule,
],
...

Also it needs to be run in ngZone: https://github.com/angular/material2/issues/9875

Then in the error-service.ts:

public handleError (error: HttpErrorResponse | any) {
  customSnackbarService.open(error, 'error')
  return throwError(error);
}
   
louiiuol
  • 3
  • 4
mchl18
  • 2,119
  • 12
  • 20
1

Use fat arrow ()=> instead of function on the handleError

public handleError = (error: HttpErrorResponse | any) => {
    this.snackBar.open('Error message: '+error.error.error, 'action', {
      duration: 4000,
    });
    return throwError(error);
  }
Makhele Sabata
  • 582
  • 6
  • 16
0

The question is about the default code what you write in the "error" function of any observable to make HTTP request and establish a generic action by default in case of get any error HTTP 4xx response from the API (for my particular case, display a MatSnackBar with the error). Well, i found the solution, ovewriting the ErrorHandle of Angular implementing this: https://angular.io/api/core/ErrorHandler

This is my XhrErrorHandlerService

import { Injectable, ErrorHandler, Injector, NgZone } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material';


@Injectable({
  providedIn: 'root'
})
export class XhrErrorHandlerService implements ErrorHandler{

  constructor(
    private injector: Injector,
    public snackBar: MatSnackBar,
    private readonly zone: NgZone
  ) {}

  handleError(error: Error | HttpErrorResponse){
    if (error instanceof HttpErrorResponse) {
      for (var i = 0; i < error.error.length; i++) {
        this.zone.run(() => {
          const snackBar = this.snackBar.open(error.error[i], error.status + ' OK', {
            verticalPosition: 'bottom',
            horizontalPosition: 'center',
            duration: 3000,
          });
          snackBar.onAction().subscribe(() => {
            snackBar.dismiss();
          })
        });
      }
    }
    else{
      console.error(error);
    }
  }
}

And this is my ng Module:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import * as moment from 'moment';

import { AppComponent } from './app.component';

//ANIMATIONS
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

//ANGULAR MATERIAL
import { MaterialModule } from './amaterial.module';

//SERVICES
import { AuthService } from './services/auth.service';
import { XhrErrorHandlerService } from './services/xhr-error-handler.service';

//RUTAS
import { RouteRoutingModule } from './route-routing.module';

//INTERCEPTOR
import { AuthInterceptor } from './http-interceptor/auth-interceptor';


@NgModule({
  declarations: [
    AppComponent,
  ],
  entryComponents: [],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MaterialModule,
    RouteRoutingModule,
    EntryComponentModule,
    HttpClientModule,
    FormsModule, 
    ReactiveFormsModule
  ],
  providers: [
    AuthService, 
    XhrErrorHandlerService,
    { provide: ErrorHandler, useClass: XhrErrorHandlerService }
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

  
Simón Farias
  • 732
  • 2
  • 8
  • 21
0

It is possible to open a snackbar in any service. The key is a proper way of using handleError() function in catchError() - context of this must be bound with the handler function.

Add MatSnackBarModule into app.module.ts imports array

import { MatSnackBarModule } from '@angular/material/snack-bar';
...
 imports: [MatSnackBarModule]
...

Use the snackbar in any service, e.g. MyService like this:

import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ApiService } from './api.service';

@Injectable()
export class MyService {
  constructor(
    private api: ApiService,
    private snackBar: MatSnackBar,
  ) {}

  getRegime(): Observable<Regime> {
    return this.api.regime().pipe(
      catchError((err) => this.handleError(err)) //bind the context of 'this' instance with '=>'
    );
  }

  getBranches(): Observable<Branch[]> {
    return this.api.branches().pipe(
       catchError(this.handleError.bind(this)) //bind the context of 'this' instance with 'bind(this)'
    );
  }

  handleError(err: any) {
    this.snackBar.open(`Failed.`, 'Ok', { panelClass: 'warn' });
    return throwError(err);
  }

}
Patronaut
  • 1,019
  • 10
  • 14