components icon indicating copy to clipboard operation
components copied to clipboard

mat-icon shows text before showing properly

Open probert94 opened this issue 6 years ago • 29 comments

Bug, feature request, or proposal:

Bug or feature request.

What is the expected behavior?

When using <mat-icon>home</mat-icon>, nothing should be displayed until the icon is loaded properly. As soon as the icon is loaded, it should render.

What is the current behavior?

Depending on the speed of your connection, it is possible, that you see the text (for example home) instead of the icon.

What are the steps to reproduce?

  1. Start the Angular Material Icon example in Stackblitz. It might be better to open it in it's own tab.
  2. Open the Developer-Tools (in Chrome) and under the Network-Tab choose a very slow network throttling profile. This is needed, as the sample app is very small and therefore the problem is not noticeable with normal connections.
  3. Reload the page and wait until it is loaded. You will see something like this: image After some time it shows the icon: image

What is the use-case or motivation for changing an existing behavior?

In an app I am currently working on, this happens pretty often. Even if I only change the route (which does not trigger a reload of the page) I often see the text of the icons before the icon is displayed properly, so it does not seem to be related to the loading of the icon font.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

I am using @angular/material 6.3.1 together with TypeScript 2.7.2 on a Windows 10 PC.
The problem occures at least on Chrome, Opera, Firefox and Edge.

probert94 avatar Jul 12 '18 12:07 probert94

I just noticed, that the problem in my app was actually the change-detection. I am using CdkPortal and CdkPortalOutlet to dynamicaly change the ocntent of my app-bar (including some mat-icons).
As I am using ChangeDetectionStrategy.OnPush, I had to call detectChanges() after changing the content. However, you can still see the text, while the font is loading. On normal connections it isn't really noticeable, but if you have a slow connection, the text stays for a while. I guess it would be good, if the icons don't show anything as long as the used font is not fully loaded. Not sure, if thats even possible...

probert94 avatar Jul 13 '18 13:07 probert94

Going to close this as "working as expected". The component on its own doesn't have a good way of knowing when the icon font is loaded. If your app knows when the assets are loaded, you can hide the icons until then, or you can potentially fall back to pngs or svgs.

jelbourn avatar Jul 16 '18 22:07 jelbourn

@jelbourn would you reopen this if the component does in fact have a good way of knowing when the font is loaded?

if ("fonts" in document && !document.fonts.check(iconFontName)){
  document.body.classList.add("mat-icon-font-missing");
  document.fonts.load(iconFontName)
    .then(() => document.body.classList.remove("mat-icon-font-missing"));
}

CSS should be easy enough once you have that. Of course, this doesn't avoid the FOUT for IE/Edge users, but there's just no helping some people.

thw0rted avatar Aug 02 '18 13:08 thw0rted

@thw0rted Interesting, I didn't know about that API. We would need some way to know the name of the icon-font being used, since it isn't always necessarily the Material Design icon font. I imagine this would become a new optional setting on the icon registry. It's something that would be good for a community PR.

jelbourn avatar Aug 02 '18 16:08 jelbourn

Is the icon registry responsible for the default behavior today? The docs say "By default, expects the Material icons font" with a link to instructions on including the @font-face definition. I was going to suggest a universal / top-level "icon font not loaded" class, but of course with the registry you can have an arbitrary number of fontSet so you'd need to do the above logic for each font in use.

And I see what you were saying in your original response, none of the current registry methods seem to care about the font name. Moreover, it appears to be possible to use font-based mat-icon without touching the registry at all -- I haven't done it myself, but it sounds like you just need to set the fontSet / fontIcon params. I guess this sort of behavior would need to either be added to a current call (like an extra parameter for registerFontClassAlias) or just be exposed as a new method.

thw0rted avatar Aug 03 '18 07:08 thw0rted

Yeah, the registry has the default icon font css class. So in addition to that class, it would need to know about the font name.

jelbourn avatar Aug 03 '18 15:08 jelbourn

Hi there,

document.body.classList.add("mat-icon-font-missing"); document.fonts.load(iconFontName)

Please keep in mind that it might break universal if you refer to classList

anymos avatar Aug 30 '18 05:08 anymos

When you say "break universal", I guess technically Material2 could support IE9 but that's the only outlier I see on the list and frankly, if you're stuck on IE9 you have bigger problems in your life 😒

thw0rted avatar Aug 30 '18 08:08 thw0rted

just checking if there is any PR WIP on this?

kwalski avatar Dec 26 '18 20:12 kwalski

When you say "break universal", I guess technically Material2 could support IE9 but that's the only outlier I see on the list and frankly, if you're stuck on IE9 you have bigger problems in your life 😒

Not sure about IE9, universal is server side rendering ;-) fortunately I am not stuck with IE9 aahahah

NodeSwapCeo avatar Mar 08 '19 04:03 NodeSwapCeo

I think I got the same issue: mat-icon displays only text when using OnPush in the parent component. When OnPush is removed everything works.

Here's a Stackblitz example with angular 8 an angular material 8: https://stackblitz.com/edit/components-issue-ghjcpf?file=app/app.component.ts

Explanation of the code: I have a custom IfDirective (which I have modified to show the problem). The directive changes every 2 seconds its ViewContainer contents.

What you should be able to see: The first 2 seconds you can see the notifications icon. After those 2 seconds the directive will clear the view container and displays the other template. This other template also contains a mat-icon, but it will always display the string instead of the icon.

When you remove changeDetection: ChangeDetectionStrategy.OnPush from the AppComponent everything works as expected. But that's no solution.

When you change the directive input variable show from within the AppComponent everything is working fine including OnPush, because in this case the component automatically runs the change detection, I guess.

The mat-raised-button with text behave normal. Animations, text, styles are in place. The problem only happens with mat-icon.

EDIT: "my" problem can be fixed by calling detectChanges() within the directive:

this.viewContainer.createEmbeddedView(this.templateRef).detectChanges();

EDIT2: Another "solution" (or workaround) that works for me, is this:

ngOnInit() {
    const ifTemplateRef = this.templateRef.createEmbeddedView(null);
    const elseTemplateRef = this.appIfAuthenticatedElse.createEmbeddedView(null);

    this.someObservable$.subscribe(val => {
        this.viewContainer.detach();
        if (val) {
            this.viewContainer.insert(ifTemplateRef);
        } else if (this.appIfAuthenticatedElse) {
            this.viewContainer.insert(elseTemplateRef);
        }
    });
}

benneq avatar Jun 07 '19 15:06 benneq

Is there a requirement for the font ligature identifier to be provided as the text inside the <mat-icon> tag? I think you can introduce an attribute for this function, just like the svgIcon or fontIcon attributes, and the html-rendering issue would be solved. e.g.:

<mat-icon ligature="home"></mat-icon>

mrmashal avatar Oct 19 '19 14:10 mrmashal

Here's how I am solving it, with fontfaceobserver:

app.component.ts

export class AppComponent {
  constructor(renderer: Renderer2) {
    const materialIcons = new FontFaceObserver('Material Icons');
    materialIcons.load(null, 10000)
      .then(() => this.renderer.addClass(document.body, `${keyword}-loaded`))
      .catch(() => this.renderer.addClass(document.body, `${keyword}-error`)); // this line not necessary for simple example
  }
}

Simple - don't show until font is loaded

scss

// don't show mat-icon until font is loaded, unless using a different font set
:not(.material-icons-loaded) .mat-icon:not([fontSet]) {
  display: none;
}

Fancy - allow fallback text if font load fails

scss

.mat-icon:not([fontSet]) {
  display: none;
  .#{$class}-loaded & {
    // loaded font successfully
    display: inline-block;
  }
  .#{$class}-error & {
    // could not load font - show fallback label if available
    display: inline-block;
    visibility: hidden;
    &[data-label]:before {
      content: attr(data-label);
      visibility: visible;
    }
  }
}

html

<!-- displays nothing -->
<mat-icon>arrow_forward</mat-icon>

<!-- displays 'submit' -->
<mat-icon [data-label]="submit">arrow_forward</mat-icon>

cbejensen avatar Oct 24 '19 20:10 cbejensen

I am working with an Angular 8 project where I am utilizing mat-icon's that were experiencing FOUT (Flash of Unstyled Text) resulting in the icon text getting displayed for a brief moment while the Web Application waited for the Material Icon font families to load. My solution was to add the npm package webfontloader

npm install --save webfontloader

Inside the app.module.ts:

import * as WebFont from 'webfontloader';

WebFont.load({
  custom: { families: ['Material Icons', 'Material Icons Outline'], }
});

And in the css/sass file where the font families are imported:

.wf-loading body {
  display: none;
}

thekarlbrown avatar Mar 06 '20 15:03 thekarlbrown

Update: I'm not so sure my suggestion below actually works!

I think what may have happened is that Firefox specifically still flashes icons during the SSR handover, while Chrome doesn't.

Just in case, here's my original comment about what I thought would fix that with Universal:


Thanks for the discussion all, and suggested workarounds @cbejensen + @thekarlbrown.

I was seeing this issue in an Angular 9 Universal app, I think most visibly on even faster connections during the handover from server to client JS (possibly a side effect / deeper issue around where things load from, but this was at least the most visible symptom).

I had to adapt these solutions to get server renders working. I tried the Typekit webfontloader first but found it had a window dependency, at least when loaded with import, which broke the SSR build.

Using fontfaceobserver got round this problem, but giving Renderer2 a document.body ref led to a runtime crash on the server side, for (in retrospect) obvious reasons.

So my approach is now based on @cbejensen's but hiding the app component instead of the body, and using ElementRef:

app.component.ts:

import { ElementRef, Inject, OnInit, Renderer2 } from '@angular/core';
import * as FontFaceObserver from 'fontfaceobserver';
// ...
export class AppComponent implements OnInit {
  constructor(
    private elementRef: ElementRef,
    @Inject(PLATFORM_ID) private platformId: Object,
    private renderer: Renderer2,
    // ...
  ) {}

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      // Avoid flash of icon placeholder text during SSR -> client JS handover.
      const materialIcons = new FontFaceObserver('Material Icons');
      materialIcons.load(null, 10000)
        .then(() => this.renderer.addClass(this.elementRef.nativeElement, 'material-icons-loaded'));

      // ...existing client-side-only inits.
    } else {
      // On the server side, add the class to show icons immediately.
      this.renderer.addClass(this.elementRef.nativeElement, 'material-icons-loaded');
    }
  }
}
...

app.component.scss:

...
app-root:not(.material-icons-loaded) {
  display: none;
}
...

Hope this helps anyone else trying to sort this with a Universal / dynamic SSR build.

NoelLH avatar Jun 03 '20 11:06 NoelLH

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts) https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

makidoll avatar Jun 25 '20 12:06 makidoll

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts) https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

Work for me !

mohamed-tebani avatar Oct 21 '20 08:10 mohamed-tebani

Would be nice to allow hide text in case of failure.

ievgennaida avatar Jan 01 '21 15:01 ievgennaida

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts) https://google.github.io/material-design-icons # Setup Method 2. Self hosting

You'll find 2 css snippets of @font-face and .material-icons In the @font-face css add font-display: block like this

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  ...

This will stop the font from showing anything until it loads.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

Update on two things

  1. Google fonts support the font-display property in the URL itself. So, self-hosting is not mandatory.

https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round&display=block"

  1. font-display: block gives only a short period of block after that it works like swap. which means, if you can throttle the network to slow 3G, you still see FOUT after some time.

PhanindraNath avatar Jan 28 '21 19:01 PhanindraNath

I know this isn't a great solution (since it's by no means reactive to when the actual icon is available for use), but it worked for our use case.

tl;dr: use ngclass and switch a boolean to display the icon after a certain time via setTimeout

  1. define a class to hide the icon, in my case this was .hide-icon { color: transparent; }

  2. ng-class based on a boolean property you define <mat-icon [ngClass]="{'hide-icon': !initialized}"> {{hidePassword ? 'visibility_off' : 'visibility'}} </mat-icon>

  3. timeout to the rescue! set the variable as false initially, and change it to true after some amount of time (we use 2 seconds) ngAfterViewInit() { setTimeout(() => { this.initialized = true; this.cdr.detectChanges(); }, 2000); }

Huiet avatar Apr 29 '21 13:04 Huiet

Here is how I managed to minimize this issue. But note that it only works for Chrome and Edge, otherwise it will fallback to the usual behaviour. By using document.fonts.onloadingdone I check if the font I want is loaded. If it is loaded I create a class which will turn the opacity of the element 'mat-icon' to 1 which was initially set as 0 in the css.

On a css file I write this rule (which will get overridden by the class which will be defined in the script eventually) mat-icon { opacity: 0 };

I added a script tag in my index html page.

// This executes on chrome and EDGE only as tested
document.fonts.onloadingdone = function (fontFaceSetEvent) {
  // console.log(fontFaceSetEvent.fontfaces) to see the available fonts which have been loaded
  // and change the font family name according to your font family requirement
  const fontName = 'Material Icons';
  if (fontFaceSetEvent.fontfaces.filter(i => i.family === fontName).length > 0) {
      addMakeIconsVisibleClass();
  }
};

// Fallback - call below function if not chrome (or EDGE)
if (navigator.userAgent.toLowerCase().indexOf('chrome') === -1) {
  addMakeIconsVisibleClass();
}

function addMakeIconsVisibleClass() {
  let style = document.createElement('style');
  style.innerHTML = 'mat-icon { opacity: 1 !important }';
  document.getElementsByTagName('head')[0].appendChild(style);
}

dillyboy avatar Aug 20 '21 23:08 dillyboy

As a very simple workaround you can preload the font via in your index.html. For instance:

<head>
  <link rel="preload" href="assets/fonts/MaterialIcons/MaterialIcons-Regular.woff2" as="font" type="font/woff2">
</head>

markiewiczart avatar Sep 21 '21 16:09 markiewiczart

I'm using the following inside the AppComponent and hiding all content until the fonts have loaded. Seems to work well.

document.fonts.ready.then(() => (this.isFontsLoaded = true));

vangorra avatar Apr 27 '22 01:04 vangorra

I came here after searching for a way to hide the text on a dynamically opened component that contains mat-tab. The list of possible hacks here is nice.

But if it comes to the main issue: when will it be fixed?

tomaszs avatar Aug 10 '22 11:08 tomaszs

For me the best solutions was to add &display=block at the end of import file @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=block');

thiagosbrito avatar Sep 01 '22 13:09 thiagosbrito

There's a really straightforward fix for this.

If you self-host your material icons (which I recommend doing with all your fonts) https://google.github.io/material-design-icons # Setup Method 2. Self hosting

Note that these instructions are wrong and have been for over three years: https://github.com/google/material-design-icons/issues/1092. Thanks, Google!

dhdaines avatar Sep 28 '23 12:09 dhdaines

The workable solution for self-hosting appears to be https://github.com/marella/material-icons these days (or npm install material-icons@latest)

dhdaines avatar Sep 28 '23 12:09 dhdaines

Hi all, I came here because I noticed my (svg) icon has a slight delay in displaying. I have a requirement to generate an image from an html node that contains the icon and, currently, I am kicking this off in the ngAfterViewInit.. unfortunately it seems like the icon has not loaded in yet at this point resulting in a missing icon in my image. Does anyone have a way to hook into when the icon has actually finished loading? is there some event trigger we can subscribe to?

JoshuaWi avatar Oct 18 '23 08:10 JoshuaWi

I suggest to use mat icons like this <mat-icon svgIcon="home"></mat-icon> instead of <mat-icon>home</mat-icon>

utamuratov avatar Feb 21 '24 07:02 utamuratov