Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using Abstract Classes As Dependency-Injection Tokens With "providedIn" Semantics In Angular 9.1.9

By Ben Nadel on

Earlier this week, I looked at saving temporary form-data to the SessionStorage API in Angular 9 so that a user wouldn't lose their form-data if they accidentally refreshed the page. In that exploration, I had a Storage class that used the SessionStorage API internally in a hard-coded fashion. But, I wanted to take a look at how I could make that behavior more dynamic using Dependency-Injection (DI) tokens. A few years ago, I wrote about using abstract classes as DI tokens in Angular 4; however, with the new providedIn @Injectable decorator, I didn't know how to do this. That is, until I came across a post on tree-shakeable providers by Manfred Steyer. What follows in my attempt to take Manfred's insights and translate them over to my temporary storage exploration in Angular 9.1.9.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

A couple of versions ago, Angular introduced a new @Injectable() decorator semantic that would allow us to "provide" Services within our Angular application without having to explicitly define those services in the @NgModule decorators:

@Injectable({
	providedIn: "root"
})
export class MyServiceClass { 
	// ....
}

I couldn't really tell you what this providedIn:"root" concept is doing mechanically; I can only tell you that it magically adds the given Service to the Dependency-Injection (DI) container. This allows other classes to then inject this Service using the traditional type-based annotations:

import { MyServiceClass } from "./my-service";

@Injectable({
	providedIn: "root"
})
export class SomeConsumerClass { 
	constructor( myService: MyServiceClass ) {
		// ....
	}
}

Now, getting back to the idea of using an abstract class as a dependency-injection token, I wasn't sure how to turn the MyServiceClass reference into an abstract class that concrete implementations could implement. I figured that I could always go and add the default, concrete implementation to the @NgModule decorator; however, that would defeat the ease-of-use that the providedIn syntax is bringing to the table.

It wasn't until I read Manfred's blog post that I even realized that the @Injectable() decorator has more than just the providedIn key. In fact, it allows for all the same "use" options that we can have in the @NgModule providers:

  • useClass
  • useValue
  • useFactory
  • useExisting

ASIDE: The documentation for the @Injectable() decorator lists providedIn as the only option, which is definitely part of my confusion on the matter.

With this new insight, we can expose an abstract class as a dependency-injection token and then use the useClass option to tell it which concrete implementation to use as the default provider.

Circling back to my temporary storage demo, I can now create a TemporaryStorageService class that is abstract, provides a default, concrete implementation, and acts as a dependency-injection token in the Angular application:

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

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

// By using an ABSTRACT CLASS as the dependency-injection (DI) token, it allows us to
// use the class as both the token and as an INTERFACE that the concrete classes have to
// implement. And, by including the "useClass" property in our decorator, it allows us
// to define the DEFAULT IMPLEMENTATION to be used with this injection token.
@Injectable({
	providedIn: "root",
	useClass: forwardRef( () => SessionStorageService ) // Default implementation.
})
export abstract class TemporaryStorageService {
	public abstract get<T>( key: string ) : Promise<T | null>;
	public abstract remove( key: string ) : void;
	public abstract set( key: string, value: any ) : void;
}

Here, we're using the abstract class, TemporaryStorageService, as both the DI token and the Interface for the concrete implementations. We're then using the useClass option to tell the Angular Injector to provide the SessionStorageService class as the default implementation for said DI token.

NOTE: I'm using the forwardRef() function here because the SessionStorageService class is defined lower-down in the code-file. I could have swapped the order of the definitions and used the class reference directly; but, I wanted to the code-file to read top-to-bottom.

Now, from my other Services and Components, I can inject the TemporaryStorageService token and remain blissfully ignorant of whichever implementation is actually being provided. To see this in action, I've created an App component that injects the TemporaryStorageService token and receives one of the following three concrete implementations:

  • SessionStorageService
  • LocalStorageService
  • InMemoryStorageService

The provided implementation is being driven by the a URL search parameter on page-refresh:

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

// Import the application components and services.
import { TemporaryStorageService } from "./temporary-storage.service";

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

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<ul>
			<li>
				<a href="./index.html?which=SessionStorageService">
					Use <code>SessionStorageService</code>
				</a>
			</li>
			<li>
				<a href="./index.html?which=LocalStorageService">
					Use <code>LocalStorageService</code>
				</a>
			</li>
			<li>
				<a href="./index.html?which=InMemoryStorageService">
					Use <code>InMemoryStorageService</code>
				</a>
			</li>
		</ul>
	`
})
export class AppComponent {

	// I initialize the app component.
	constructor( temporaryStorage: TemporaryStorageService ) {

		// With dependency-injection (DI), all we're doing is asking the DI container
		// for a class that implements the "TYPE" of TemporaryStorageService. The
		// implementation of said type is of little concern. Let's look at which
		// implementation was injection into this component.
		console.group( "Injected Storage Service" );
		console.log( temporaryStorage );
		console.groupEnd();

		// Let's test the implementation to make sure it handles the Set / Get workflow.
		(async function() {

			var key = "Hello";
			var value = "World";
			temporaryStorage.set( key, value );

			console.group( "Testing Set / Get" );
			console.log( "Set:", `${ key } -> ${ value }` );
			console.log( "Get:", await temporaryStorage.get( key ) );
			console.groupEnd();

		})();

	}

}

As you can see, this App components provides three links which refresh the page with a new ?which URL search parameter. For the sake of the demo, I'm parsing the URL and defining the DI token overrides in my App module. In the following code, take note that I'm not doing anything for the default implementation, SessionStorageService - I'm only providing an override for the other two, non-default implementations:

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

// Import the application components and services.
import { AppComponent } from "./app.component";
import { InMemoryStorageService } from "./temporary-storage.service";
import { LocalStorageService } from "./temporary-storage.service";
import { TemporaryStorageService } from "./temporary-storage.service";

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

var providers: Provider[] = [];

console.group( "Parsing Provider From URL" );

// Parse the "which={type}" out of the search string.
switch ( getWhichParamFromUrl() ) {
	case "SessionStorageService":

		console.log( "Found:", "SessionStorageService" );
		// This is the default implementation, don't do anything.

	break;
	case "LocalStorageService":

		console.log( "Found:", "LocalStorageService" );
		// For the LocalStorage implementation, all we have to do is tell Angular
		// Injector to use the given class (LocalStorageService) when one of the other
		// classes requests the "TemporaryStorageService" injection token.
		providers.push({
			provide: TemporaryStorageService,
			useClass: LocalStorageService
		});

	break;
	case "InMemoryStorageService":

		console.log( "Found:", "InMemoryStorageService" );
		// For the In-Memory implementation, all we have to do is tell Angular Injector
		// to use the given class (InMemoryStorageService) when one of the other classes
		// requests the "TemporaryStorageService" injection token.
		providers.push({
			provide: TemporaryStorageService,
			useClass: InMemoryStorageService
		});

	break;
}

console.groupEnd();

@NgModule({
	imports: [
		BrowserModule
	],
	providers: providers,
	declarations: [
		AppComponent
	],
	bootstrap: [
		AppComponent
	]
})
export class AppModule {
	// ...
}

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

// I parse the "which" parameter from the URL (for the demo, not super robust).
function getWhichParamFromUrl() : string {

	var matches = window.location.search.match( /((?<=\bwhich=)[^&]+)/i );

	return( ( matches && matches[ 0 ] ) || "SessionStorage" );

}

As you can see, for two of the three URL parameters, I'm defining an override to the TemporaryStorageService DI token (and abstract class). And, if we run this Angular app in the browser and click through the links, we get the following console log output:

Using an abstract class as a dependency-injection (DI) token in Angular 9.1.9 allows for swappable behaviors.

How cool is that?! By using an abstract class as the dependency-injection token in conjunction with the useClass @Injectable() option, we're able to keep the simplicity of the providedIn syntax while also allowing for the traditional override functionality of Providers. It's the best of both worlds!

Here's the full code for my TemporaryStorageService code-file. It includes the abstract class as well as the three concrete implementations:

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

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

// By using an ABSTRACT CLASS as the dependency-injection (DI) token, it allows us to
// use the class as both the token and as an INTERFACE that the concrete classes have to
// implement. And, by including the "useClass" property in our decorator, it allows us
// to define the DEFAULT IMPLEMENTATION to be used with this injection token.
@Injectable({
	providedIn: "root",
	useClass: forwardRef( () => SessionStorageService ) // Default implementation.
})
export abstract class TemporaryStorageService {
	public abstract get<T>( key: string ) : Promise<T | null>;
	public abstract remove( key: string ) : void;
	public abstract set( key: string, value: any ) : void;
}

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

interface StorageCache {
	[ key: string ]: any;
}

@Injectable({
	providedIn: "root"
})
export class SessionStorageService implements TemporaryStorageService {

	private cache: StorageCache;
	private storageKey: string;

	// I initialize the session-storage service.
	constructor() {

		this.cache = Object.create( null );
		this.storageKey = "temporary_storage";

	}

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

	// I get the value stored at the given key; or null if undefined.
	public async get<T>( key: string ) : Promise<T | null> {

		return( <T>this.cache[ key ] ?? null );

	}


	// I remove the value stored at the given key.
	public remove( key: string ) : void {

		if ( key in this.cache ) {

			delete( this.cache[ key ] );
			this.persistCache();

		}

	}


	// I store the given value with the given key.
	public set( key: string, value: any ) : void {

		this.cache[ key ] = value;
		this.persistCache();

	}

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

	// I load the in-memory cache from the SessionStorage API.
	private loadCache() : void {

		var serializedData = window.sessionStorage.getItem( this.storageKey );

		if ( serializedData ) {

			Object.assign( this.cache, JSON.parse( serializedData ) );

		}

	}


	// I persist the in-memory cache to the SessionStorage API.
	private persistCache() : void {

		// TODO: Wrap this in a debounced-timer so that we're not constantly flushing the
		// in-memory cache to the SYNCHRONOUS SessionStorage API. But, this is beyond the
		// scope and goal of this demo.
		window.sessionStorage.setItem( this.storageKey, JSON.stringify( this.cache ) );

	}

}

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

@Injectable({
	providedIn: "root"
})
export class LocalStorageService implements TemporaryStorageService {

	private cache: StorageCache;
	private storageKey: string;

	// I initialize the local-storage service.
	constructor() {

		this.cache = Object.create( null );
		this.storageKey = "temporary_storage";

	}

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

	// I get the value stored at the given key; or null if undefined.
	public async get<T>( key: string ) : Promise<T | null> {

		return( <T>this.cache[ key ] ?? null );

	}


	// I remove the value stored at the given key.
	public remove( key: string ) : void {

		if ( key in this.cache ) {

			delete( this.cache[ key ] );
			this.persistCache();

		}

	}


	// I store the given value with the given key.
	public set( key: string, value: any ) : void {

		this.cache[ key ] = value;
		this.persistCache();

	}

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

	// I load the in-memory cache from the LocalStorage API.
	private loadCache() : void {

		var serializedData = window.localStorage.getItem( this.storageKey );

		if ( serializedData ) {

			Object.assign( this.cache, JSON.parse( serializedData ) );

		}

	}


	// I persist the in-memory cache to the LocalStorage API.
	private persistCache() : void {

		// TODO: Wrap this in a debounced-timer so that we're not constantly flushing the
		// in-memory cache to the SYNCHRONOUS LocalStorage API. But, this is beyond the
		// scope and goal of this demo.
		window.localStorage.setItem( this.storageKey, JSON.stringify( this.cache ) );

	}

}

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

@Injectable({
	providedIn: "root"
})
export class InMemoryStorageService implements TemporaryStorageService {

	private cache: StorageCache;

	// I initialize the in-memory storage service.
	constructor() {

		this.cache = Object.create( null );

	}

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

	// I get the value stored at the given key; or null if undefined.
	public async get<T>( key: string ) : Promise<T | null> {

		return( <T>this.cache[ key ] ?? null );

	}


	// I remove the value stored at the given key.
	public remove( key: string ) : void {

		delete( this.cache[ key ] );

	}


	// I store the given value with the given key.
	public set( key: string, value: any ) : void {

		this.cache[ key ] = value;

	}

}

Dependency-injection (DI) is simply magical. I suspect it's a huge part of why people who start using Angular can't imagine switching over to another web-application framework. And, I'm loving that the new(ish) providedIn syntax provides a simple syntax while also allowing for dynamic overrides. Angular is the bee's knees!

Epilogue on the core ErrorHandler Service in Angular

The Angular framework provides an ErrorHandler class that is used to log errors to the console (by default). This class can be injected into your services and components using a default implementation; but, it can also be overridden using a Provider, much like we did in this blog post.

However, when you look at the ErrorHandler code on GitHub, what you will see is that they do not use an abstract class. Instead, they side-step all of the pitfalls of "using a Class as an Interface" by marking everything but the public methods as /** @internal */.

I don't really understand what this @internal thing is doing; but, it appears to remove the annotated methods and properties from appearing in the Type Definition file. As such, when you - as a developer - go to author a class that implements ErrorHandler, you don't also have to implement all of the private methods and properties that Angular defined internally.

I don't have much to say about that - I don't even know if @internal is annotation that we can use in our own applications. I just thought it was an interesting bit of information.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
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.