Skip to main content
Ben Nadel at the MySQL NYC Meetup (Oct. 2024) with: David Baird
Ben Nadel at the MySQL NYC Meetup (Oct. 2024) with: David Baird

Loading And Using Remote Feature Flags In Angular 9.0.0-next.12

By
Published in Comments (4)

The other day, I wrote about using feature flags to conditionally render routable components in Angular 9. After that post, it occurred to me that I've talked a lot about how much we love feature flags here at InVision; but, I've never really looked at how we get feature flags into an Angular application. As such, I wanted to put together a simple demo that loads remote feature flags into an Angular 9.0.0-next.12 application as part of the bootstrapping and initialization process.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When it comes to getting configuration data into an Angular application, you have three options:

Regardless of the methodology, the consumption within the Angular application is generally the same: the data gets folded into some sort of a service Provider that can then be injected into other factories and Classes using Angular's Dependency Injection (DI) framework. For the purposes of this exploration, I'm going to use the AJAX (Asynchronous JavaScript and JSON) approach as I believe that this is likely to be the majority use-case for Single-Page Applications (SPA) written in Angular.

Since this Angular demo is being hosted on a static server (GitHub Pages), I don't have an "API" that can serve-up dynamic, user-specific data. As such, I'm representing my server's API using a hard-coded JSON (JavaScript Object Notation) file:

{
	"user": {
		"id": 1,
		"email": "ben@bennadel.com"
	},
	"company": {
		"id": 101,
		"name": "Dig Deep Fitness"
	},
	"featureFlags": {
		"feature-a": true,
		"feature-b": false,
		"feature-c": true
	}
}

This JSON file is going to be requested during the Angular bootstrapping process and the featureFlags key is going to be folded into the Angular Dependency Injection framework. And, since feature flags may play a critical role in the way that the Angular application is configured, we want to block the application bootstrapping process until these feature flags have been retrieved from the remote API.

Luckily, Angular natively supports asynchronous configuration using the APP_INITIALIZER provider. The APP_INITIALIZER provider is just a collection of functions that have to be resolved before the Angular application completes the bootstrapping process. We're going to use these functions in our root module to trigger an AJAX request using the AppConfig class.

The AppConfig class acts as both a data fetcher and a Service provider that can then be injected into other providers and services. Let's look at the AppConfig class first:

// Import the core angular services.
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";

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

@Injectable({
	providedIn: "root"
})
export class AppConfig {

	// In the properties list, we're using the "DEFINITE ASSIGNMENT ASSERTION" (!) so
	// that we don't have to provide default values for all of the properties that are
	// going to be overwritten by the remote configuration data. As part of the
	// Application Bootstrapping logic, we know that this class will never be used until
	// the .loadRemoteConfig() method runs and the results have been appended to the
	// instance. As such, by the time the rest of the classes inject the AppConfig, these
	// data structures will all have been created / populated.
	public featureFlags!: {
		"feature-a": boolean;
		"feature-b": boolean;
		"feature-c": boolean;
	};

	private httpClient: HttpClient;

	// I initialize the app initializer.
	constructor( httpClient: HttpClient ) {

		this.httpClient = httpClient;

	}

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

	// I load the remote configuration file into the instance.
	// --
	// CAUTION: This method is being called as part of the application bootstrapping
	// process. Changes to this logic here may affect the ability for the Angular
	// application to start.
	public async loadRemoteConfig() : Promise<void> {

		try {

			var remoteConfig = await this.httpClient
				.get<AppConfig>( "assets/api/config.json" )
				.toPromise()
			;

			// The intention here is for the remote configuration payload to have the
			// same structure as the AppConfig object. As such, we should just be able to
			// merge the remote structure into the local structure and all of the
			// "definite assignment assertions" will come into fruition.
			Object.assign( this, remoteConfig );

			console.group( "Remote Configuration Loaded" );
			console.log( remoteConfig );
			console.groupEnd();

		} catch ( error ) {

			console.group( "Remote Configuration Failed" );
			console.error( error );
			console.groupEnd();

			// NOTE: Throwing this error will prevent the application from bootstrapping.
			throw( error );

		}

	}

}

The AppConfig class serves two purposes:

  1. It loads the remote user-specific configuration data through the .loadRemoteConfig() method.

  2. It acts as a Provider of the aforementioned configuration data by merging said data into the public API of the class (ie, the .featureFlags public property).

ASIDE: The Object.assign() call is also merging-in the .user and .company keys that are present in the remote config.json file; however, in order to keep the demo simple, I am only defining the .featureFlags key.

Note that the .loadRemoteConfig() method returns a Promise. This Promise will be used to block the bootstrapping process of the Angular application until the AppConfig class has been populated with the remote data. And, once the AppConfig class is hydrated and the Promise has been resolved, the rest of the Angular app can bootstrap and consume the AppConfig public API.

Let's look at the root app module to see how the APP_INITIALIZER is configured:

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

// Import the application components and services.
import { AppComponent } from "./app.component";
import { AppConfig } from "./app-config.service";

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

@NgModule({
	imports: [
		BrowserModule,
		HttpClientModule
	],
	providers: [
		// The APP_INITIALIZER multi-provider allows for additional asynchronous
		// configuration to take place during the bootstrapping process. These
		// initializers will "block" the application until the returned Promises are
		// resolved, which allows us to make a request to the remote server in order to
		// gather user-specific or environment-specific configuration values that are NOT
		// available at compile-time.
		{
			provide: APP_INITIALIZER,
			useFactory: function( appConfig: AppConfig ) {

				async function asyncInitializer() : Promise<void> {

					// The .loadRemoteConfig() method will trigger an AJAX request that
					// will populate the AppConfig instance. This way, it will contain
					// the remote configuration data by the time that it gets injected
					// into other Providers and Classes.
					await appConfig.loadRemoteConfig();

				}

				// NOTE: The factory function returns the asynchronous FUNCTION - it does
				// not execute the function directly.
				return( asyncInitializer );

			},
			deps: [ AppConfig ],
			multi: true
		}
	],
	declarations: [
		AppComponent
	],
	bootstrap: [
		AppComponent
	]
})
export class AppModule {
	// ...
}

As you can see, the APP_INITIALIZER factory function just returns a function that invokes our AppConfig.loadRemoteConfig() method. And, again, this will block the bootstrapping process until the resultant Promise resolves, allowing the AppConfig class time to hydrate with user-specific data before the rest of the Services and Components get instantiated.

Once the asynchronous data fetch resolves, folding it into the rest of the Angular application is done on as as-needed basis. Meaning, you could do any of the following:

  • Push the remote data into other services that have been injected into the AppConfig class.

  • Expose the remote data as an injectable Provider using other Factory functions.

  • Leave the remote data in the AppConfig, and then treat the AppConfig class as an injectable Provider that can be consumed by other Services and Components.

That last option is the most straightforward option; and, the option that I am using in this demo. Once the AppConfig has requested the remote data and the feature flags have been merged into its own public API, I'm going to treat the AppConfig as a server Provider. This service will then be injected into my Angular components, where the exposed feature flags can drive rendering and other decision logic.

To see this in action, I'm going to inject the AppConfig service into my root Angular component where I'm going to conditionally render parts of the HTML template:

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

// Import the application components and services.
import { AppConfig } from "./app-config.service";

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

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<!--
			NOTE: Feature flags are NOTHING MORE than indicators of whether or not some
			logic branch in the application should be used. DON'T OVERTHINK IT. You don't
			need special directives that "handle the feature flag" for you. Angular
			already has structural directives that do just that: manage logic branching.
			In thise case, I'm just using the *ngIf directive to show or hide markup
			based on the user's feature flag settings.
		-->

		<ng-template [ngIf]="isShowingFeatureA">
			<p>
				Woot! You have <strong>Feature-A</strong>!
			</p>
		</ng-template>

		<ng-template [ngIf]="isShowingFeatureB">
			<p>
				Noice! You have <strong>Feature-B</strong>!
			</p>
		</ng-template>

		<ng-template [ngIf]="isShowingFeatureC">
			<p>
				Sweet! You have <strong>Feature-C</strong>!
			</p>
		</ng-template>
	`
})
export class AppComponent {

	public isShowingFeatureA: boolean;
	public isShowingFeatureB: boolean;
	public isShowingFeatureC: boolean;

	// I initialize the app component.
	constructor( appConfig: AppConfig ) {

		// Once the app has been initialized, the AppConfig instance will contain all of
		// the feature flags for this user. At this point, it's as simple as checking the
		// values and seeing if they are turned On or Off.
		this.isShowingFeatureA = appConfig.featureFlags[ "feature-a" ];
		this.isShowingFeatureB = appConfig.featureFlags[ "feature-b" ];
		this.isShowingFeatureC = appConfig.featureFlags[ "feature-c" ];

	}

}

As you can see, the AppConfig service has been fully populated with remote data by the time the root component is instantiated. This is only possible because the Angular application bootstrapping process was temporarily blocked by the APP_INITIALIZER, which triggered the AJAX call to the remote API server.

And, once we have the AppConfig service injected, all we have to do is pull values out of the .featureFlags property. In this case, we're using these feature flags to power a few ngIf directives. And, when we run the above Angular code, we get the following output:

Loading and consuming remote feature flags in an Angular 9 application.

As you can see, the two enabled feature flags result in two rendered portions of the HTML template. And, the one disabled feature flag results in a hidden portion of the HTML template.

And, that's really all there is to consuming feature flags in an Angular application. It doesn't get much more complicated than that. I have seen "Feature Flag Libraries" that include special directives that help you render template partials based on feature flag state. But, if you look at what these directives are doing, they are, basically, glorified ngIf directives. As such, there's no need to create any higher-level abstraction: Angular already has all the tools you need to load and consume feature flags.

Static vs. Dynamic Feature Flags In A Single-Page Application

In this exploration, the feature flags are "static". Meaning, they are loaded dynamically during the bootstrapping process; but then, they remain static (unchanged) for the rest of the Angular application life-span. This may or may not be acceptable for your context. Personally, this is the only approach that I have used to-date. It makes the application a easier to reason about; and, much easier to build since you don't have to worry about values changing dynamically. That said, there's nothing stopping you from updating the feature flags throughout the user's experience. But, that goes beyond what I can talk about with any confidence.

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

Reader Comments

449 Comments

This is fabulous.

I have been looking for a way to inject variables into Angular, before the rest of the application has initialized.

Previously, I have used:

index.cfm

And then add a snippet of JS [local example], which redirects to my Angular Application:

location.href = "http://localhost:4200?featureFlag1=#featureFlag1#";

I then find out which Angular component renders first, which is usually:

app.component.ts

I then capture the URL variables using a JS function.

But this is Jank central.

Your approach is much more elegant.

15,902 Comments

@Charles,

Awesome my man, glad you found this helpful. If you're using ColdFusion (.cfm) files anyway, then you can definitely make an API call to a dynamic-endpoint in the way that I have outlined. What actually serves-up your Angular app? Is that just a static site somewhere?

449 Comments

Well. My first attempt was a bit of a mess but it worked.

I had the following structure:

src
src/assets/cfm/index.cfm
src/app

And then the:

src/assets/cfm/index.cfm

Has the JS redirect, back to:

src/index.html

But, most Angular guys have told me not place the cfm files inside the Angular assets directory.

So, I was thinking of doing something like:

rest/taffy
rest/resources
rest/index.cfm

src
src/assets/images etc
src/app

In this way, I separate my Angular Application from my Coldfusion files. As you say, I can then make a REST call to bring in my config settings, using your methodology.

But, the answer to your original question, is that I use Lucee on a VPS, so I can pretty much do anything I want.

15,902 Comments

@Charles,

Yeah, keep the things well separated is not something I have a great instinct for. In my "production" app, it's so old, and uses Gulp scripts to do all the compilation (not to mention that it is still using AngularJS 1.2.22 :scream:). And then, all of the new R&D that I do with Angular is usually only Angular; so, I don't have a good instinct for it.

In the few cases that I have tried, it does end up looking like what you have in your latter example. I usually have some sort of /api folder that houses my ColdFusion / Lucee code. But, that only works locally because I never have a live demo that runs ColdFusion (since most of my stuff just runs on GitHub pages).

So, at least I think we are both moving in the same general direction.

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