In an agile development environment, there may be partially completed features or features with external dependencies that are not ready. Instead of relying on multiple code branches, instead you may opt for deploying code with a feature turned off. Later, that feature should be turned on via a configuration or database change. This post provides sample code that you can use to implement feature flags in your Angular app.
Option 1: Define features in a config file
One option for storing feature flags is in a JSON configuration file. Refer to this post for an explanation on consuming an editable configuration file.
config.dev.json
{ . . . "features": { "feature1": true, "feature2": false } }
app-config.model.ts
export interface IAppConfig { . . . features: { [name: string]: boolean; }; }
Create a feature flag service
Centralize the management of determining whether a feature is turned on or off into an Angular service.
feature-flag.service.ts
import { Injectable } from '@angular/core'; import { AppConfig } from '../../app.config'; @Injectable() export class FeatureFlagService { featureOff(featureName: string) { // Read the value from the config service if (AppConfig.settings.features.hasOwnProperty(featureName)) { return !AppConfig.settings.features[featureName]; } return true; // if feature not found, default to turned off } featureOn(featureName: string) { return !this.featureOff(featureName); } }
Option 2: Use an API instead of a config file to get feature flags
Instead of reading feature flags from a config file, another option is to retrieve the features from the server via an API that could read these values from a database. The feature-flag service could be adapted to fetch the feature flag values as soon as the app starts.
feature-flag.service.ts
export class FeatureFlagService { private _featureFlags: Array<string> = []; // A list of all features turned ON private _initialized = false; constructor(private featureFlagDataService: FeatureFlagDataService) { } featureOff(featureName: string) { return !this.featureOn(featureName); } featureOn(featureName: string) { if (!featureName) { return true; } // Find the feature flag that is turned on return this._featureFlags && !!this._featureFlags.find(feature => { return feature === featureName; }); // if feature not found, default to turned off } get initialized() { return this._initialized; } // This method is called once and a list of features is stored in memory initialize() { this._featureFlags = []; return new Promise((resolve, reject) => { // Call API to retrieve the list of features and their state // In this case, the method returns a Promise, // but it could have been implemented as an Observable this.featureFlagDataService.getFeatureFlags() .then(featureFlags => { this._featureFlags = featureFlags; this._initialized = true; resolve(); }) .catch((e) => { resolve(); }); }); }
Create attribute directives to hide and disable elements
To hide an element based on whether a feature is turned on or off, use the following code to create a directive. This will enable the Angular templates to use this syntax:
<div [myRemoveIfFeatureOff]="'feature1'"> <div [myRemoveIfFeatureOn]="'feature2'">
remove-if-feature-off.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core'; import { FeatureFlagService } from '../../services/feature-flag.service'; @Directive({ selector: '[myRemoveIfFeatureOff]' }) export class MyRemoveIfFeatureOffDirective implements OnInit { @Input('myRemoveIfFeatureOff') featureName: string; constructor(private el: ElementRef, private featureFlagService: FeatureFlagService) { } ngOnInit() { if (this.featureFlagService.featureOff(this.featureName)) { this.el.nativeElement.parentNode.removeChild(this.el.nativeElement); } } }
remove-if-feature-on.directive.ts
. . . @Directive({ selector: '[myRemoveIfFeatureOn]' }) export class MyRemoveIfFeatureOnDirective implements OnInit { @Input('myRemoveIfFeatureOn') featureName: string; // Same as myRemoveIfFeatureOff except call featureOn() }
Call the feature flag service elsewhere in the app
In addition to the attribute directives, the feature flag service can be called throughout the app. For instance, a menu service could use the featureOn method to hide menu items if the user does not have the required permission.
menu.service.ts
private showMenuItem(featureName: string) { return this.featureFlagService.featureOn(featureName); }
Prevent routing to component
If access to a component should be prevented when a feature is turned off, this can be accomplished by using the canActivate option available in Angular’s router.
my-routing.module.ts
{ path: 'my-component', component: MyComponent, canActivate: [FeatureFlagGuardService], data: { featureFlag: 'feature1' } },
feature-flag-guard.service.ts
@Injectable() export class FeatureFlagGuardService implements CanActivate { constructor(private featureFlagService: FeatureFlagService) { } canActivate(route: ActivatedRouteSnapshot): Promise<boolean> | boolean { // Get the name of the feature flag from the route data provided const featureFlag = route.data['featureFlag']; if (this.featureFlagService.initialized) { if (featureFlag) { return this.featureFlagService.featureOn(featureFlag); } return true; // no feature flag supplied for this route } else { const promise = new Promise<boolean>((resolve, reject) => { this.featureFlagService.initialize() .then(() => { if (featureFlag) { resolve(this.featureFlagService.featureOn(featureFlag)); } else { resolve(true); } }).catch(() => { resolve(false); }); }); return promise; } } }
This implementation of feature flags has proven to be helpful on my current project and has allowed us to proceed with the development of new features without the risk of introducing new functionality before QA is ready. I’ve introduced a few enhancements along the way, including:
- addition of a scope for the feature – meaning the feature is available only for certain criteria
- an option for the route guard to check for any of an array of features’ being turned on
if (this.featureFlagService.anyFeatureOn(['feature1', 'feature2', 'feature3'])) { canActivate: [FeatureFlagGuardService], data: { anyFeatureFlags: ['feature1', 'feature2', 'feature3'] } }
Also, it is worth highlighting that if a feature name is misspelled either in the configuration or in the code, then it is considered turned off. The effect of this approach is that a feature will not accidentally be turned on.
No comments:
Post a Comment