DEV Community

Justin
Justin

Posted on • Originally published at Medium

Feature flags in Angular

Recently, I have found myself more convinced about using feature flags in my Angular projects. I was skeptical at first because the code can become quite a mess and hard to maintain properly. But, I have come to appreciate the pros.

I have collaborated with clients who followed a bi-weekly release schedule that came with some challenges. Tying feature releases to these fixed schedules made it really restrictive. We couldn't always adhere to, for example, the legal department. All of our feature releases happened on the scheduled release. However, with the adoption of feature flags, the flexibility to release on any day became feasible. Moreover, the feature flags enable smoother rollbacks in the face of unforeseen issues.

Another advantage is that feature flags enable the developer to create small pull requests. They can merge slices of a feature without immediately deploying.

What is a feature flag?

A feature flag, also known as a feature toggle or feature switch, is a programming technique that allows developers to turn specific features or functionalities of a software application on or off, usually during runtime. 

As an illustration, in this article, we will be discussing the fastLogin feature flag. The feature enables a streamlined login process, making it into a single step. Without the feature flag, you would have to contact customer support, which gives you a code to log in (bad UX, but you get the point).

On the left the application with the fastLogin feature disabled. It shows a wireframe of an application. The wireframe has the text: "Contact customer support to login" with a contact button. On the right a wireframe with fastLogin enabled. This shows two inputs username and password with a button to log in.

Getting started

In this example, we will be using an API to retrieve the feature flags that are active, but you could also use a json file in the code, up to you. First, let's define the structure of the API response.

// feature-flag.service.ts

type FeatureFlagResponse = {
  fastLogin: boolean;
  fastRegister: boolean;
  fastSettings: boolean;
}
Enter fullscreen mode Exit fullscreen mode

The FeatureFlagResponse consists of features returned by the API. In this case fastLogin, fastRegister and fastSettings.

Managing active feature flags can become quite cumbersome. It is crucial to sync the types with the backend. So it is clear which feature flags are active.

By using the keys within the FeatureFlagResponse, we can scope our functions to only have access to the keys in the FeatureFlagResponse. This narrows down the potential feature flags you could use in the front end. If it doesn't exist in the response, you can't use it.

// feature-flag.service.ts

type _FeatureFlagKeys = keyof FeatureFlagResponse;
Enter fullscreen mode Exit fullscreen mode

It is worth noting that I have opted against using only keyof FeatureFlagResponse. By transforming the types, the output becomes "fastLogin" | "fastRegister" | "fastSettings", providing a more user-friendly experience when inspecting types.

// feature-flag.service.ts

export type FeatureFlagKeys = {
  [K in _FeatureFlagKeys]: K;
}[_FeatureFlagKeys]
Enter fullscreen mode Exit fullscreen mode

Shows the infobox in Visual Studio Code when you hover over an variable. In this case the _FeatureFlagsKeys. The infobox says the type is keyof FeatureFlagsResponse.

Shows the infobox in Visual Studio Code when you hover over an variable. In this case the FeatureFlagsKeys. The infobox says the type is fastLogin, fastRegister or fastSettings.

Now, let's integrate the method to retrieve feature flags and store them into a signal. The signal is being used to verify whether a specific feature is currently enabled or not.

// feature-flag.service.ts

@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
  http = inject(HttpClient);
  features = signal<Record<string, boolean>>({});

  getFeatureFlags(): Observable<FeatureFlagResponse> {
    return this.http.get<FeatureFlagResponse>('/api/flags').pipe(tap((features) => this.features.set(features)));
  }

  getFeature(feature: FeatureFlagKeys): boolean {
    return this.features()[feature] ?? false;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can leverage the service to invoke the API, but what is the optimal place for this action? A suitable moment is during app initialization, and fortunately, Angular offers a DI token to provide one or more initialization functions.

By creating a custom provider function we can adhere to the new Angular standard by calling provideFeatureFlag(). The factory is being used to initialize the feature flags.

// feature-flag.provider.ts

function initializeFeatureFlag(): () => Observable<any> {
  const featureFlagService = inject(FeatureFlagService);
  return () => featureFlagService.getFeatureFlags();
}

export const provideFeatureFlag = () => ({
  provide: APP_INITIALIZER,
  useFactory: initializeFeatureFlag,
  deps: [],
  multi: true,
})
Enter fullscreen mode Exit fullscreen mode
// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideHttpClient(),
    provideFeatureFlag(),
  ],
};
Enter fullscreen mode Exit fullscreen mode

The feature flags are now accessible from everywhere in the app. The next consideration is integrating them into component templates. One approach could be injecting the FeatureFlagService wherever it is required. Alternatively, we could use a structural directive.

// feature-flag.directive.ts

@Directive({ selector: '[featureFlag]', standalone: true })
export class FeatureFlagDirective {
  templateRef = inject(TemplateRef);
  viewContainer = inject(ViewContainerRef);
  featureFlagService = inject(FeatureFlagService);
  hasView = signal(false);

  @Input() set featureFlag(feature: FeatureFlagKeys) {
    if (this.featureFlagService.getFeature(feature) && !this.hasView()) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView.set(true);
    } else {
      this.viewContainer.clear();
      this.hasView.set(false);
    }
  }

  @Input() set featureFlagElse(elseTemplateRef: TemplateRef<any>) {
    if (!this.hasView()) {
      this.viewContainer.createEmbeddedView(elseTemplateRef);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This directive makes it possible to display content only when a specified feature is enabled. Additionally, it offers the flexibility to include an alternative template for when the feature is not enabled. For example when fastLogin is enabled we can log in with one click. If it's not enabled, we will have to mail the customer support.

<div *featureFlag="'fastLogin'; else notEnabled">
  <button>Login with one click</button>
</div>
<ng-template #notEnabled>
  <div>Mail us one support@example.com for your login.</div>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

It is worth mentioning that the FeatureFlagKeys from earlier ensure that the autocomplete feature exclusively suggests the options typed in the FeatureFlagResponse such as fastLogin, fastRegister and fastSettings.

The autocomplete feature in Visual Studio Code shows immediately fastLogin, fastRegister and fastSettings.

Attempting a random string will result in a build error, ensuring we stick to the set feature flags.

Build error in Visual Studio Code because it is using a non existing feature flag as input for the directive.

This strict typing not only makes the development process smoother but also acts as a strong mechanism for quickly spotting references to feature flags that have been removed, making the cleanup process much easier.

But, what about routes? We could create a route guard that utilizes the service as well.

// feature-flag.guard.ts
export const featureFlagGuard = (feature: FeatureFlagKeys) => {
  return () => {
    const featureFlagService = inject(FeatureFlagService);
    return featureFlagService.getFeature(feature);
  };
};

// routes.ts
{
  path: 'fast-register',
  canMatch: [featureFlagGuard('fastRegister')],
  loadComponent: () => ..,
}
Enter fullscreen mode Exit fullscreen mode

Or you could just use it inline.

canMatch: [() => inject(FeatureFlagService).getFeature('fastLogin')]
Enter fullscreen mode Exit fullscreen mode

And there we go. Feature flags in Angular. If you have questions or use cases that we could tackle. Please let me know.

Top comments (1)

Collapse
 
oidacra profile image
Arcadio Quintero

Good post Justin.