Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (May. 2009) with: Gert Franz
Ben Nadel at the New York ColdFusion User Group (May. 2009) with: Gert Franz@gert_railo )

Creating A Dynamic Favicon Service In Angular 5.2.4

By Ben Nadel on

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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.