Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney

Creating A Dynamic Favicon Service In Angular 5.2.4

By
Published in Comments (17)

A little while ago, GitHub added a feature to their Pull Request (PR) pages in which the browser's favicon would reflect the state of the PR (pending, failed, approved, etc.). This way, you could use other browser tabs and still maintain some sense of how your PR was progressing. I think this is a delightful user experience (UX) detail; and, it got me noodling on how I might integrate similar behavior into an Angular 5.2.4 application in such a way that I don't overly couple my application components to the implementation details of the favicon itself.

Run this demo in my JavaScript Demos project on GitHub.

At its core, a Favicon (favorite icon) is just a Link element in the Head of the current page's HTML document. The Link element contains a type attribute and an href attribute that define which graphics file will be used to render the favicon. And while I want various Angular components to be able to set this Favicon, I didn't want the Angular components to have to worry about how to inject this Link element; or, even to worry about where files were located or what type they were (ex, ico vs. png).

To encapsulate the implementation details, I moved the file definitions to the application bootstrapping process. There, I defined a service provider that associated the individual favicons with unique name-based tokens:

// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

// Import the application components and services.
import { AppComponent } from "./app.component";
import { BROWSER_FAVICONS_CONFIG } from "./favicons";
import { BrowserFavicons } from "./favicons";
import { Favicons } from "./favicons";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	bootstrap: [
		AppComponent
	],
	imports: [
		BrowserModule
	],
	declarations: [
		AppComponent
	],
	providers: [
		// The Favicons is an abstract class that represents the dependency-injection
		// token and the API contract. THe BrowserFavicon is the browser-oriented
		// implementation of the service.
		{
			provide: Favicons,
			useClass: BrowserFavicons
		},
		// The BROWSER_FAVICONS_CONFIG sets up the favicon definitions for the browser-
		// based implementation. This way, the rest of the application only needs to know
		// the identifiers (ie, "happy", "default") - it doesn't need to know the paths
		// or the types. This allows the favicons to be modified independently without
		// coupling too tightly to the rest of the code.
		{
			provide: BROWSER_FAVICONS_CONFIG,
			useValue: {
				icons: {
					"square": {
						type: "image/png",
						href: "./icons/default.png",
						isDefault: true
					},
					"happy": {
						type: "image/jpeg",
						href: "./icons/happy.jpg"
					},
					"indifferent": {
						type: "image/png",
						href: "./icons/indifferent.png"
					},
					"sad": {
						type: "image/jpeg",
						href: "./icons/sad.jpg"
					}
				},

				// I determine whether or not a random token is auto-appended to the HREF
				// values whenever an icon is injected into the document.
				cacheBusting: true
			}
		}
	]
})
export class AppModule {
	// ...
}

As you can see, the BROWSER_FAVICONS_CONFIG service provider contains a collection of identifiers, such as "happy" and "sad", that represent the implementation details of the injected HTML Link Element. With this association provided to the Favicons implementation, the rest of the application need only know about the identifiers and can remain blissfully unaware of file paths and mime-types.

NOTE: If there packaged up as module (outside the scope of this demo), I could have used the .forRoot() module pattern, which would have kept the entire definition process a bit cleaner.

To see what I mean, let's look at the app component that receives and consumes the Favicons service:

// Import the core angular services.
import { Component } from "@angular/core";

// Import the application components and services.
import { Favicons } from "./favicons";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			Select the favicon to use:
		</p>

		<ul>
			<li>
				<a (click)="useFavicon( 'happy' )">Happy</a>
			</li>
			<li>
				<a (click)="useFavicon( 'indifferent' )">Indifferent</a>
			</li>
			<li>
				<a (click)="useFavicon( 'sad' )">Sad</a>
			</li>
		</ul>

		<p>
			<a (click)="resetFavicon()">Reset the Favicon</a>
		</p>
	`
})
export class AppComponent {

	private favicons: Favicons;

	// I initialize the app component.
	constructor( favicons: Favicons ) {

		this.favicons = favicons;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called once after the inputs have been bound.
	public ngOnInit() : void {

		this.resetFavicon();

	}


	// I reset the favicon to use the "default" item.
	public resetFavicon() : void {

		console.log( "Resetting favicon" );
		this.favicons.reset();

	}


	// I activate the favicon with the given name.
	public useFavicon( name: string ) : void {

		console.log( "Activating favicon:", name );
		// Notice that we don't need to know anything about how the favicon is defined;
		// not URLs, no image types - just the identifier. All of the implementation
		// details have been defined at bootstrap time.
		this.favicons.activate( name );

	}

}

As you can see in this code, when the Application component wants to use a particular favicon, it doesn't deal with any file paths - it simply asks the injected Favicons service to activate the favicon associated with the given identifier (such as "happy"). The Favicons service implementation then handles all of the DOM (Document Object Model) node construction and injection. This allows the favicon details to change in one place without having a ripple effect propagate throughout the entire Angular application.

The Favicons service itself is fairly simple. It's little more than a wrapper around some DOM API calls. The only thing of particular interest in this module is the fact that I'm using an abstract class as the dependency-injection (DI) token. This abstract class provides a way for other developers to create their own platform-specific implementations without having to do some funky "extends" nonsense; or, have to change the DI token being used throughout the rest of the application. This abstract class creates a very clean separation between the concept of the favicon and the platoform-oriented implementation.

// Import the core angular services.
import { Inject } from "@angular/core";
import { InjectionToken } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

export interface FaviconsConfig {
	icons: IconsConfig;
	cacheBusting?: boolean;
}

export interface IconsConfig {
	[ name: string ]: IconConfig;
}

export interface IconConfig {
	type: string;
	href: string;
	isDefault?: boolean;
}

export var BROWSER_FAVICONS_CONFIG = new InjectionToken<FaviconsConfig>( "Favicons Configuration" );

// This abstract class acts as both the interface for implementation (for any developer
// that wants to create an alternate implementation) and as the dependency-injection
// token that the rest of the application can use.
export abstract class Favicons {
	abstract activate( name: string ) : void;
	abstract reset() : void;
}

// I provide the browser-oriented implementation of the Favicons class.
export class BrowserFavicons implements Favicons {

	private elementId: string;
	private icons: IconsConfig;
	private useCacheBusting: boolean;

	// I initialize the Favicons service.
	constructor( @Inject( BROWSER_FAVICONS_CONFIG ) config: FaviconsConfig ) {

		this.elementId = "favicons-service-injected-node";
		this.icons = Object.assign( Object.create( null ), config.icons );
		this.useCacheBusting = ( config.cacheBusting || false );

		// Since the document may have a static favicon definition, we want to strip out
		// any exisitng elements that are attempting to define a favicon. This way, there
		// is only one favicon element on the page at a time.
		this.removeExternalLinkElements();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I activate the favicon with the given name / identifier.
	public activate( name: string ) : void {

		if ( ! this.icons[ name ] ) {

			throw( new Error( `Favicon for [ ${ name } ] not found.` ) );

		}

		this.setNode( this.icons[ name ].type, this.icons[ name ].href );

	}


	// I activate the default favicon (with isDefault set to True).
	public reset() : void {

		for ( var name of Object.keys( this.icons ) ) {

			var icon = this.icons[ name ];

			if ( icon.isDefault ) {

				this.setNode( icon.type, icon.href );
				return;

			}

		}

		// If we made it this far, none of the favicons were flagged as default. In that
		// case, let's just remove the favicon node altogether.
		this.removeNode();

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I inject the favicon element into the document header.
	private addNode( type: string, href: string ) : void {

		var linkElement = document.createElement( "link" );
		linkElement.setAttribute( "id", this.elementId );
		linkElement.setAttribute( "rel", "icon" );
		linkElement.setAttribute( "type", type );
		linkElement.setAttribute( "href", href );
		document.head.appendChild( linkElement );

	}


	// I return an augmented HREF value with a cache-busting query-string parameter.
	private cacheBustHref( href: string ) : string {

		var augmentedHref = ( href.indexOf( "?" ) === -1 )
			? `${ href }?faviconCacheBust=${ Date.now() }`
			: `${ href }&faviconCacheBust=${ Date.now() }`
		;

		return( augmentedHref );

	}


	// I remove any favicon nodes that are not controlled by this service.
	private removeExternalLinkElements() : void {

		var linkElements = document.querySelectorAll( "link[ rel ~= 'icon' i]" );

		for ( var linkElement of Array.from( linkElements ) ) {

			linkElement.parentNode.removeChild( linkElement );

		}

	}


	// I remove the favicon node from the document header.
	private removeNode() : void {

		var linkElement = document.head.querySelector( "#" + this.elementId );

		if ( linkElement ) {

			document.head.removeChild( linkElement );

		}

	}


	// I remove the existing favicon node and inject a new favicon node with the given
	// element settings.
	private setNode( type: string, href: string ) : void {

		var augmentedHref = this.useCacheBusting
			? this.cacheBustHref( href )
			: href
		;

		this.removeNode();
		this.addNode( type, augmentedHref );

	}

}

As you can see, the .activate() method takes the string-based identifier provided by the Angular application components and turns it into a Link element that gets injected into the document's Head tag.

Now, if we run the application and select one of the favicons, we get the following output:

Dynamic favicons in an Angular 5.2.4 application.

As you can see, when we select the "happy" favicon, a new Link element is injected / replaced into the document Head, which alters the icon that shows up in the browser tab.

To be fair, I don't know all that much about favicons. In this exploration, I'm using PNG and JPG files. But, you might prefer to use an ICO file that aggregates multiple sizes within it. The good news is, you can choose whatever you want in the configuration options and the application doesn't have to know about it. The Favicons service creates a clean point of encapsulation between the Angular components and the browser platform.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Thanks a lot for taking the time to write up this post - definitely saved me a lot of time! Using this to display a favicon spinner while slow-running reports are retrieving data in my app.

1 Comments

Used this in conjunction with environmental variables to change favicons based on the environment. Thanks so much.

I'm getting some strange behaviors where despite finding the file successfully the favicon changes to the not found icon (the blank page), any idea why?

15,902 Comments

@Eugene,

Hmm, I'm not sure why that would be happening. Did you check the network activity in the dev-tools to make sure the file is really being located? Also, perhaps the file type does not match the extension and the browser isn't rendering it property? I'm just guessing -- I've not seen that before.

2 Comments

Used it but it's not working in Edge and IE browser any idea for running that in Edge and interner explorer browser.

15,902 Comments

@Nirmal,

Interesting. When I open this particular demo in IE11, I get a Zone.js syntax error. I wonder if there was a zone bug in whatever version this was running in. What version of Angular are you using?

15,902 Comments

@Nirmal,

I am not sure. I have a few things I need to investigate with an IE VirtualMachine. I'll have to update mine (it's using too-old of a browser).

15,902 Comments

@Maxime1992,

Looks pretty cool. I'm not familiar with the angular.json file. I am assuming that's part of the angular-cli. I keep meaning to look into the CLI. I am sure it enables features that I haven't really used yet. Good work!

1 Comments

I'm looking to use a image returned in an api and set it to the favicon. How do I import the favicon service for that?

15,902 Comments

@Abhi,

My particular implementation wouldn't work with that since it expects all of the Favicon sources to be known at compile-time. However, I have seen other implementations that use a "registration" approach. So, rather than just supplying the sources at compile time, the sources get registered. This allows you to register new sources at runtime and then activate them.

That said, you could more-or-less make that work here by adding an additional method like activateSource(), which takes an image source and by-passes the name-based lookup. Just shooting from the hip here:

public activateSource( iconHref: string, iconType: string = "png" ) : void {

	this.setNode( iconType, iconHref );

}

So, this would by-pass the validation of the name-based look-up and just set the Favicon using the Href / Type that you pass-in.

1 Comments

For those getting an error in Internet Explorer and MS Edge, try removing the trailing "i" in this line: document.querySelectorAll( "link[ rel ~= 'icon' i]" );

IE and Edge don't support the case-insensitive flag in its query selectors.

15,902 Comments

@Kalen,

Thanks for the tip. It's funny, I don't even remember typing i in there :D When I saw your note, I thought I had probably just made a typo. In fact, I had to re-look-up that you could even flag a selector as being case-insensitive. I think this must have been the only time I've ever used that.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel