Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jason Dean and Simon Free and Alison Huselid and Josh Adams and John Mason
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jason Dean@JasonPDean ) , Simon Free@simonfree ) , Alison Huselid , Josh Adams@jladams97 ) , and John Mason@john_mason_ )

Adding An Angular 14 Front-End To My ColdFusion Feature Flag Exploration

By on

About a month ago, I posted Strangler: Building a Feature Flag System in ColdFusion. That proof-of-concept was constructed in Lucee CFML using a standard post-back workflow wherein each navigation begot a full page refresh. Over the last few weeks, I've been dribbling some effort into creating a thick-client experience using Angular 14. The UI (User Interface) still leaves a lot to be desired; but, I think as a second-stage proof-of-concept, there's enough here to be demoed.

View this code in my Strangler project on GitHub.

It's been quite a while since I've built anything of any complexity in modern Angular (my day-to-day work still involves AngularJS 1.7). And, seeing as this Angular UI includes routing, services, and API calls, it ended up having a lot of moving parts despite its relatively small size and scope. Needless to say, I am feeling rather rusty when it comes to modern front-end architecture.

There's way too much code in this Angular app to show in a single post; so, I think maybe the only part that I'll share outside of the GitHub repo is the TypeScript definition for the Feature Flag data-model. Having not really touched TypeScript in a while, thinking in terms of Interfaces entailed a bit of trial-and-error, writing some TypeScript code and then seeing if it would compile.

The complexity with the Feature Flag data model is that it is not uniform. There are several different types of flags (Boolean, Numeric, String, Any). And, there are several different types of operators (User-key, User-property). And, there are several different types of distribution models (Single, Multi).

I ended up using a healthy amount of Discriminating Unions, which allow a dynamic portion of a Type-tree to change based on the existence of a static, known value. So, for example, to create the various types of feature flags, I first created a BaseFeatureFlag followed by several members of a discriminating union that extend the BaseFeatureFlag. Then, the top-level feature flag is a basic Type union of the lower-level feature flags:

export namespace FeatureFlags {

	interface BaseFeatureFlag {
		key: string;
		name: string;
		description: string;
		rules: Rule[];
		fallthroughVariantRef: number;
		isEnabled: boolean;
	}

	export interface AnyFeatureFlag extends BaseFeatureFlag {
		type: "Any";
		variants: any[];
	}

	export interface BooleanFeatureFlag extends BaseFeatureFlag {
		type: "Boolean";
		variants: boolean[];
	}

	export interface NumericFeatureFlag extends BaseFeatureFlag {
		type: "Numeric";
		variants: number[];
	}

	export interface StringFeatureFlag extends BaseFeatureFlag {
		type: "String";
		variants: string[];
	}

	export type FeatureFlag =
		| AnyFeatureFlag
		| BooleanFeatureFlag
		| NumericFeatureFlag
		| StringFeatureFlag
	;

	// ... truncated ....
}

Here, the type property is the discriminator which is what allows the variants property to have a different type for each feature flag. Then, you can see that the top-level FeatureFlag is just a union of all four discriminated types.

I used this same approach for the different types of Test configurations:

export namespace FeatureFlags {

	// ... truncated ....

	export interface UserKeyTest {
		type: "UserKey";
		operation: Operation;
	}

	export interface UserPropertyTest {
		type: "UserProperty";
		userProperty: string;
		operation: Operation;
	}

	export type Test =
		| UserKeyTest
		| UserPropertyTest
	;

	// ... truncated ....
}

And with the distribution models as well:

export namespace FeatureFlags {

	// ... truncated ....

	export interface SingleRollout {
		type: "Single";
		variantRef: number;
	}

	export interface MultiRollout {
		type: "Multi";
		distribution: Distribution[];
	}

	export type Rollout =
		| SingleRollout
		| MultiRollout
	;

	// ... truncated ....
}

Once I had these discriminated unions in place, I could start dynamically changing parts of this complex feature flag data model and the TypeScript compiler was happy to oblige.

There's likely a lot of code in this Angular 14 app that people won't agree with. I prefer Promises over RxJS Steams; and, I prefer letting my Components load their own data over performing data-loading in route-guards. But, I believe at the end of the day, this code is - at least - fairly easy to reason about. If nothing else, it felt good to tip my toes back in the Angular pool.

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

Reader Comments

15,325 Comments

After publishing this Angular 14 UI, I kept feeling like something was amiss in my error handling. As such, I went back and added Type Guards and Type Narrowing into my error handling workflow:

www.bennadel.com/blog/4324-using-type-guards-to-narrow-down-error-handling-types-in-angular-14.htm

This allows me to narrow down my error types from the default any to the ErrorResponse interface being returned by my ApiClient. This is what I love about TypeScript - that is forces me to think more deeply about the code. That's the real value, right there!

Post A Comment — I'd Love To Hear From You!

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.