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

Packaging A Password-Strength Module In Angular 11.0.5

By Ben Nadel on

I write a lot of Angular and AngularJS code. But, in the vast, vast majority of cases, I just extend the existing "application module", adding components, directives, and services to the same bag of uniquely-named dependency-injection (DI) tokens. This year, however, I really want to start thinking more deeply about packaging cohesive sets of functionality into their own modules. I don't actually care about the modules themselves - I'm primarily concerned with boundary considerations and creating a clean separation of concerns. To this end, as a thought-experiment, I wanted to look at packaging a "Password Strength" module in Angular 11.0.5.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

I think "Password Strength" makes for an interesting exploration because it does almost nothing; so, it's not that much code to write. But, it has enough complexity that it's an interesting set of considerations when it comes to consumption, dependency-injection, and overriding providers. For this exploration, I'm going to create a module that contains the following:

  • Password Strength Service - This service exposes a method that takes a password input value and returns a "Strength". The strength rating is just a number that has no inherent meaning. The module will ship with a default implementation of this service; but, the consuming application can override the service provider in order to define their own strength calculations.

  • (Strength) Event Directive - This directive can be attached to an input control and will emit a (strength) output event whenever the input value is changed. This event will contain the number calculated by passing the input value to the above Password Strength Service.

  • Strength Indicator Component - The component can visualize the strength of a given password. It exposes a [strength] input binding and will render a strength meter along with a String-based representation of the calculation (ex, "Very Weak").

  • Strength Enum - This enumeration just maps human-friendly values (ex, strength.VERY_WEAK) onto a number. It used internally to the Strength Indicator Component; but, it could always be used by the consuming application as well.

First, let's look at the PasswordStrengthService. This service translates a given password value into a strength calculation. As I mentioned above, we want the Module to provide a default implementation; but, we also want the consuming application to be able to provide its own override. As such, we're going to define the PasswordStrengthService as an abstract class. This way, we can use the abstract class as dependency-injection token as well as using it as the "interface" that the consuming application would have to extend or implements in its override.

To keep things simple, the password strength calculation interface is going to require a synchronous call. And, the default implementation is going to depend solely on length of the input:

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

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

// Ultimately, the password strength will evaluate to a NUMBER; but, in order to make our
// internal code more intelligible, I'm going to use an ENUM to associate some human-
// consumable values to the given numbers. This way, if the parent application wants to
// implement their own set of Strength calculations, they just have to evaluate to a
// number - they don't necessarily have to use our internal set of numbers.
export enum Strength {
	VERY_WEAK = 1,
	WEAK = 2,
	GOOD = 3,
	STRONG = 4,
	EXCELLENT = 5
}

// We're going to use an Abstract Class as our "injectable" so that the parent
// application can override the strength calculations provider.
// --
// NOTE: The Password Strength MODULE is going to provide a default implementation for
// this dependency-injection token using the "useClass" semantics below.
@Injectable({
	providedIn: "root",
	useClass: forwardRef( () => DefaultPasswordStrengthServiceImplementation )
})
export abstract class PasswordStrengthService {

	public abstract evaluatePassword( value: string ) : number;

}

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

// Our DEFAULT IMPLEMENTATION for the password strength calculation will just use the raw
// length of the input to determine the strength.
@Injectable({
	providedIn: "root"
})
export class DefaultPasswordStrengthServiceImplementation extends PasswordStrengthService {

	// I evaluate the strength of the given password value.
	public evaluatePassword( value: string ) : number {

		if ( value.length <= 5 ) {

			return( Strength.VERY_WEAK );

		} else if ( value.length <= 10 ) {

			return( Strength.WEAK );

		} else if ( value.length <= 15 ) {

			return( Strength.GOOD );

		} else if ( value.length <= 20 ) {

			return( Strength.STRONG );

		} else {

			return( Strength.EXCELLENT );

		}

	}

}

As you can see, PasswordStrengthService exposes a single method, evaluatePassword() which takes a password and returns a number. Note that the method returns number and not Strength. This is important because I didn't want to lock the parent application into the same set of strength calculation tiers. By using number, it grants more flexibility to any override.

Now that we have a service that performs the calculation, and provides a mechanism whereby the parent application can use its own implementation, let's look at how the parent application might actually perform the strength calculation in the context of an input control. One option would be for a Component to inject the PasswordStrengthService "type" and perform the calculation explicitly in the business logic. But, I thought it would be more interesting if we could actually move the flow of logic into the template.

To do this, I created an attribute directive [passwordStrength] which, when added to an input, will listen for (input) events and will, in turn, emit its own (strength) event using the underlying PasswordStrengthService implementation. Note that this directive is part of the Password Strength Module:

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

// Import the application components and services.
import { PasswordStrengthService } from "./password-strength.service";

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

@Directive({
	selector: "input[passwordStrength]",
	outputs: [ "strengthEvents: strength" ],
	host: {
		"(input)": "evaluateInputValue( $event.target.value )"
	},
	exportAs: "passwordStrength"
})
export class StrengthEventDirective {

	public strength: number;
	public strengthEvents: EventEmitter<number>;

	private passwordStrengthService: PasswordStrengthService;

	// I initialize the strength-event directive.
	constructor( passwordStrengthService: PasswordStrengthService ) {

		this.passwordStrengthService = passwordStrengthService;
		this.strength = 1;
		this.strengthEvents = new EventEmitter();

	}

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

	// I evaluate the given password and emit a corresponding strength event. 
	public evaluateInputValue( value: string ) : void {

		this.strength = this.passwordStrengthService.evaluatePassword( value );
		this.strengthEvents.emit( this.strength );

	}

}

Notice that this directive is using the abstract class as the dependency-injection token for the PasswordStrengthService. At runtime, this may inject the "default implementation"; or, it may inject an override that the parent application has provided. That's the magic of dependency-injection! This directive doesn't have to care about the implementation details - it just needs a service that adheres to a given interface.

In and of itself, this directive really isn't doing much: it's just template glue. It's binding the (input) events from the password input to the .evaluatePassword() method of the service; which is then, in turn, being bound to a (strength) output event on the directive. This allows a parent template to then do something like this:

<input passwordStrength (strength)="showStrength( $event )" />

... where the $event value is the numeric calculation returned by the PasswordStrengthService.

As a convenience, I wanted the Password Strength Module to also provide some form of visualization. And, to that end, I created a StrengthIndicatorComponent which exposes an input binding, [strength], and renders a strength meter. This way, the developer can take the (strength) event from above and pipe it directly into the visualization.

The StrengthIndicatorComponent has no real logic in it; it just has the one input binding:

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

// Import the application components and services.
import { Strength } from "./password-strength.service";

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

@Component({
	selector: "password-strength-indicator",
	inputs: [ "strength" ],
	changeDetection: ChangeDetectionStrategy.OnPush,
	styleUrls: [ "./strength-indicator.component.less" ],
	templateUrl: "./strength-indicator.component.html"
})
export class StrengthIndicatorComponent {

	public strength: Strength = Strength.VERY_WEAK;

}

NOTE: As I'm writing this, I'm realizing that the type of the strength input should maybe just be number, not Strength. The reason for that is, as I mentioned earlier, I don't want the parent application to be locked into the same set of levels. That said, the view portion of this component is tied to the Strength values, so I am not sure.

And, the HTML template for this just maps that strength value onto something visual:

<dl class="header">
	<dt class="header__title">
		Password Strength
	</dt>
	<dd class="header__strength" [ngSwitch]="strength">
		<span *ngSwitchCase="1">
			Very Weak
		</span>
		<span *ngSwitchCase="2">
			Weak
		</span>
		<span *ngSwitchCase="3">
			Good
		</span>
		<span *ngSwitchCase="4">
			Strong
		</span>
		<span *ngSwitchCase="5">
			Excellent
		</span>
	</dd>
</dl>

<div class="meter meter--{{ strength }}">
	<span class="meter__strength"></span>
</div>

So far, things are pretty simple: as in, there's not that much logic going on here. And, now that we have our Service, our event Directive, and our visualization Component, we need to package it up into a Module that our parent application can consume. All this module really has to do is export the the Directive and the Component - the Service provider will implicitly be provided thanks to the @Injectable() decorator.

// Import the core angular services.
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";

// Import the application components and services.
import { DefaultPasswordStrengthServiceImplementation } from "./password-strength.service";
import { PasswordStrengthService } from "./password-strength.service";
import { Strength } from "./password-strength.service";
import { StrengthEventDirective } from "./strength-event.directive";
import { StrengthIndicatorComponent } from "./strength-indicator.component";

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

export { PasswordStrengthService };
export { Strength };
// NOTE: These don't really need to be exported for this particular demo. However, if a
// component in the parent directive were to implement a View-Child query (for example)
// that attempted to reference one of these directives, the consuming code may need to
// use these exported values as the "Type" of said query results.
export { StrengthEventDirective };
export { StrengthIndicatorComponent };

@NgModule({
	imports: [
		CommonModule
	],
	exports: [
		StrengthEventDirective,
		StrengthIndicatorComponent
	],
	providers: [
		// NOTE: We're providing the default implementation of the PasswordStrengthService
		// via the "useClass" semantics of the @Injectable() decorator.
	],
	declarations: [
		StrengthEventDirective,
		StrengthIndicatorComponent
	]
})
export class PasswordStrengthModule {
	// ...
}

Notice that this TypeScript file is explicitly exporting some services, ex:

export { PasswordStrengthService };

In this approach, I'm using the .module.ts file both as the definition of our Password Strength module but also as a barrel. This allows the parent application to access class references using this module without having to know how the files are organized within the module itself. You can this of this as defining the "public API" for the module's file-system.

And, you can see exactly how that "public API" works in the Application Module, which both imports the module as well as several of the exposed services. To keep things exciting, I'm going to conditionally provide an override of the PasswordStrenthService based on the URL search-string:

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

// Import the application components and services.
import { AppComponent } from "./app.component";
import { PasswordStrengthModule } from "./password-strength/password-strength.module";
import { PasswordStrengthService } from "./password-strength/password-strength.module";
import { Strength } from "./password-strength/password-strength.module";

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

// I can override the PasswordStrengthModule's default implementation of the password
// strength calculation.
export class SillyPasswordStrengthService implements PasswordStrengthService {

	// I evaluate the strength of the given password value.
	public evaluatePassword( value: string ) : number {

		console.log( "Evaluating silly password:", value );

		if ( value.length < 20 ) {

			return( Strength.VERY_WEAK );

		} else {

			return( Strength.EXCELLENT );

		}

	}

}

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

var demoProviders: Provider[] = [];

// If the query-string starts with "silly" we're going to OVERRIDE the password strength
// service, using our "silly" version above. Otherwise, we'll use the default one that
// comes withe module. Notice that we don't have to override anything else!
if ( window.location.search.indexOf( "?silly" ) === 0 ) {

	console.warn( "Using silly override service!" );

	demoProviders.push({
		provide: PasswordStrengthService,
		useClass: SillyPasswordStrengthService
	});

}

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

Notice that all of the Password Module services are being imported from:

./password-strength/password-strength.module

The App module doesn't have to care about where those references are actually being defined on the file-system because the Password Strength Module is "rolling them up" into its public API.

Also notice that we're using the imported PasswordStrengthService both as an interface that our override implements; and, as our dependency-injection token. Again, how freaking cool is Angular and TypeScript?!

And, finally, to pull this whole thing together, we have an App component that includes a password input and renders the strength of the provided password:

<p>
	<!-- Allow the demo to switch between the default and override services. -->
	<a href="?default">Use default calculator</a> ,
	<a href="?silly">Use silly calculator</a>
</p>

<p>
	Please enter a new password:
</p>

<p>
	<!--
		NOTE: The [passwordStrength] directive emits a (strength) event; however, in our
		demo, the view-model doesn't need to know about the strength value directly. As
		such, instead of binding to an event, we're just going to grab a template-local
		reference to the Directive and use that to power the indicator input-binding.
	-->
	<input
		type="password"
		passwordStrength
		#passwordRef
		#password="passwordStrength"
	/>
</p>

<password-strength-indicator
	*ngIf="passwordRef.value"
	[strength]="password.strength"
	class="indicator">
</password-strength-indicator>

There's very little code here; but, there's actually some interesting mechanisms at play. If you look at the input control, you'll see that it is definite two template-local variables:

  • #passwordRef - This is creating a template-local variable for the <input> DOM element itself. This allows us to check the .value of the input in the subsequent *ngIf binding.

  • #password - This is creating a template-local variable for the [passwordStrength] directive instance. This allows us to access the underlying .strength property of the calculation. With this in place, we actually don't even need to worry about the (strength) event, which means we don't have to define any methods or properties for tracking that event in our App component.

Angular is hella sweet! And, when we run this Angular 11 code in the browser, we get the following output:

Using both versions of the password strength calculator in the demo application.

As you can see, in the first input, we're using the default implementation of the PasswordStrengthService, which includes a nicely tiered set of strength calculations. Then, in the second input, we're using our "silly" override which just has "Very Weak" and "Excellent" strength calculations with no nuance in between. However, in both cases, we use the same [passwordStrength] directive and the same <password-strength-indicator> component. As such, the only thing that our parent application had to override was the calculation logic itself.

I don't have all that much experience with defining modules in Angular. As I said above, I mostly just jam everything into the main Application module (or a Feature module for lazy-loading routes). This was a learning exercise for me; and hopefully, nothing I said here was too far from correct. If nothing else, I think it helps showcase just how absurdly awesome dependency-injection is; and, how powerful Angular directives are as the thing that can glue everything together.

Epilogue on the Strength Enum vs number.

After putting this all together, I think maybe it was a mistake to try and mix the Strength enumeration with the number type. My intention was originally to have Strength so that I could make some code more human-friendly while still allowing the parent application to create an arbitrary set of strength tiers. But, in the end, I think that's a bit problematic, especially if I want to use the provided Directive / Component with a PasswordStrengthService override.

In the end, I think the better approach would have been to commit to the Strength enumeration across the board; but, make sure to give it enough gradation such that an override could comfortably use it even if it didn't use all of the tiers. This would, if nothing else, allow all code to assume that 1 / .VERY_WEAK was always the default strength.



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.