Wednesday, June 17, 2020

Angular How-to: Implement Feature Flags



 how to turn features on and off via configuration or from a database. This may include hiding and disabling UI elements, changing code flow, or preventing routing to components.

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

Free hosting web sites and features -2024

  Interesting  summary about hosting and their offers. I still host my web site https://talash.azurewebsites.net with zero cost on Azure as ...