1

I have an angular mat-icon with custom svg icon. I'm rendering the svg icon dynamically. Now in-case if the actual svg icon is missing, how can I set a default(static) svg icon.

Something similar to onError attr in img tag.

Thanks, Shihad

html:
<mat-icon svgIcon="{{row.username}}" //default_svg_icon_src// ></mat-icon>

component:
  this.users.forEach(user => {
    this.matIconRegistry.addSvgIcon(
      user.username,
      this.domSanitizer.bypassSecurityTrustResourceUrl("../assets/icons/"+user.username+".svg")
    );
  })
chrismclarke
  • 1,995
  • 10
  • 16
Shihad Salam
  • 79
  • 2
  • 11

2 Answers2

2

You will first need to check if the icon exists before passing it to the registry, and then passing your fallback icon instead if it does not.

You could do this either via a http request or creating a new image element and binding to the onload and onerror functions.

Example using http:

app.component.html

<div *ngIf="iconsLoaded">
  <div *ngFor = "let user of users">
    <mat-icon [svgIcon]="user"></mat-icon>
    <span>{{user}}</span>
  </div>
</div>  

app.component.ts

export class AppComponent  {
  users = ['user1','user2','user3','user4']
  iconsLoaded:boolean
  constructor(private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, private http:HttpClient){
    this.addUserIcons()
  }

  async addUserIcons(){
    for(let user of this.users){
      const iconPath = await this.getIconPathWithFallback(user)
      this.iconRegistry.addSvgIcon(user, this.sanitizer.bypassSecurityTrustResourceUrl(iconPath));
    }
    // let the dom know when all icons have been loaded and block attempt rendering before (e.g. via *ngIf)
    this.iconsLoaded=true
  }

  // use a get request to see if the svg exists, if not return a default svg
  async getIconPathWithFallback(user:string){
      const iconPath = `assets/icons/${user}.svg`
      const response = await this.http.head(iconPath,{responseType: 'blob'}).toPromise()
      // if the svg icon is found the response will have type svg. If file not found returns html
      console.log('response type',response.type)
      return response.type === 'image/svg+xml' ? iconPath : 'assets/icons/default.svg'
  }   
}

Here is a working stackblitz: https://stackblitz.com/edit/angular-ppbzvn

Output
example output

If you were to use a standard <img> tag instead of <mat-icon> it would be possible to just bind to the onerror call directly to replace the url if the image can't be found. This is discussed in another post here:

how can i check if image exists in assets folder using angular 4 and Angular2: check file existance with http.get

chrismclarke
  • 1,995
  • 10
  • 16
  • Tried the http request, but not getting any response. `this.users.forEach(user => { let iconName = user.username; if(!this.checkAssetExists(iconName)) { iconName = "default"; } this.matIconRegistry.addSvgIcon( user.username, this.domSanitizer.bypassSecurityTrustResourceUrl("../assets/icons/"+iconName+".svg") ); })` – Shihad Salam Aug 25 '19 at 06:28
  • `async checkAssetExists(iconName) { try { await this.http.get("../assets/icons/"+iconName+".svg", {responseType: 'text'}).toPromise() return true } catch(error) { return false } }` – Shihad Salam Aug 25 '19 at 06:29
  • Very strange, although I've realised an extra caveta is that even a failed request will return a text response (html). Have created a working stackblitz and will update answer above – chrismclarke Aug 25 '19 at 09:30
  • 1
    My bad..! I used the promise directly in checking the boolean condition. Now I tried the `then` method on the promise object to get the actual boolean value and it worked. Thanks! – Shihad Salam Aug 25 '19 at 13:41
2

The accepted answer has 2 major problems:

  1. it proposes in fact to register the icons asynchronously - so the consuming component must wait for the registration to have been completed, otherwise the icons will not be rendered (and of course waiting is bad for performance)
  2. it's not scalable: imagine we have a very large number of icons we wish to register - then we have a very large number of HTTP requests just for registering the icons.

A better solution: use an interceptor!

@Injectable()
export class SvgFallbackInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest<unknown>, next: HttpHandler) {
        const regex = /\.svg$/;
        const isSvg = regex.test(req.url);

        if (isSvg) {
            return next.handle(req).pipe(
                catchError(err => {
                    const defaultResponse = new HttpResponse({
                        body:       `<svg> ...[your default svg]...</svg>`,
                        headers:    err.headers,
                        status:     200, // or perhaps 302?
                        statusText: 'OK',
                        url:        err.url,
                    });

                    return of(defaultResponse);
                })
            );
        }
        else {
            return next.handle(req);
        }
    }
}

read more about Angular interceptors here

Eden Ilan
  • 176
  • 1
  • 5