DEV Community

Alexander Goncharuk for This is Angular

Posted on

A better feature flag directive in Angular

Custom structural directives are powerful. They promote a declarative approach and help you keep your Angular components DRY and clean. In this article, we are going to build a simple yet useful structural directive that will show one or another piece of the template depending on the value of the feature flag. Here is what we expect from our feature flag directive's public interface:

  • Directive will allow conditionally showing parts of the template depending on the feature flag's status.
  • No additional code should be needed in the component class apart from importing the directive itself to make it available in the template.

How is this one better

Directives like this are quite common in Angular projects. But with a bit of additional knowledge, we can build a more powerful directive by implementing an option to use an alternative template to show in case the feature flag's value is falsy. This option is very convenient, and yet it is not used as often as it deserves, at least based on my experience working with several Angular codebases.

If you have used *ngIf=A; else B syntax in your Angular templates, you already know that it is possible to have a behavior like this. Here is how our structural directive will be used in the component's template:

  <div *appIfFeatureFlag="'FEATURE_1'; else: defaultTemplate">Feature 1 template</div>
  <ng-template #defaultTemplate>Default template</ng-template>
Enter fullscreen mode Exit fullscreen mode

We will also use some APIs that Angular team introduced not so long time ago. In particular, standalone directives and the inject function as an alternative to injecting via the constructor's arguments.

Why feature flag use case

We could pick any shared feature for the structural directive demo purpose. I'd hence like to say a couple of words about the reasoning behind picking the feature flags use case before we start.

The path to building bug-proof applications is long and challenging. It entails many things to learn and many practices to adopt. You need very good test coverage, correct abstractions, short development cycle to name a few. Introducing feature flags takes nowhere near as much time as mastering the above-mentioned practices. By no means do feature flags replace these things, but they are the right first step on this path. You get the option to roll back problematic changes in any environment within seconds without redeploying.

I strongly believe that feature flags are a must-have thing in any application that runs in production.

This is crucial both for users who are no longer forced to use the broken feature waiting for the fix, and for developers who are thus able to address issues in non-stressful conditions.

Implementation

Without further ado, let's create the directive. We will start with the directive's Inputs and dependencies:

@Directive({
  selector: '[appIfFeatureFlag]',
  standalone: true
})
export class FeatureFlagDirective implements OnInit {
  @Input()
  appIfFeatureFlag!: string;
  @Input()
  appIfFeatureFlagElse?: TemplateRef<unknown>;

  private templateRef = inject(TemplateRef<unknown>);
  private viewContainerRef = inject(ViewContainerRef);
  private featureFlagService = inject(FEATURE_FLAGS_SERVICE);

  //...
}
Enter fullscreen mode Exit fullscreen mode

We are using standalone: true to make our directive importable directly without NgModule. And define two inputs for the name of the feature flag we will be checking and the alternative template to show in case of falsy flag value. The code is written in Typescript strict mode, so we explicitly mark optional and mandatory inputs with corresponding assertion operators.

Pay attention to how these inputs are named.

To use the desired syntax in templates we need to prefix all additional inputs with the name of the directive, which is appIfFeatureFlag in our case.

Hence, if we want to specify the alternative ng-template to show in the else parameter, we should name the relevant input appIfFeatureFlagElse.

Bear in mind how the last part of the input's name written in camel case turns into lower case automatically when the directive is used in the template:

<div *appIfFeatureFlag="'FEATURE_1'; else: defaultTemplate">Feature</div>
Enter fullscreen mode Exit fullscreen mode

And then we inject the following providers:

  • templateRef - holds the reference to the template we want to show if the feature flag in question is turned on, i.e. the content of the element *appIfFeatureFlag is attached to.

If we removed the * symbol at the beginning of the directive's name we would get a No provider for TemplateRef found error. As you see, we don't specify anywhere in the @Directive decorator's metadata that this is a structural directive as opposed to attribute directive. Instead, the * symbol serves as syntactic sugar for us, wrapping the host element in the following construction:

<ng-template [appIfFeatureFlag]="'FEATURE_1'">
  <div>Feature</div>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

The dependencies directive injects are:

  • viewContainerRef - the container used to create embedded views withing the current host element

  • featureFlagService - the service exposing APIs to work with feature flags provider. We will shed some light on good practices when working with similar dependencies further in the article.

Since Angular 14 we get these providers without injecting them via the constructor with help of the inject function that now can be called during the component's construction phase.

Now let's implement the view creation logic:

  async ngOnInit() {
    try {
      const featureFlag = await this.featureFlagService.getFeatureFlag(this.appIfFeatureFlag);
      featureFlag ? this.onIf() : this.onElse();
    } catch (error) {
      this.onElse();
      // additional error handling logic goes here
    }
  }

  private onIf(): void {
    this.createView(this.templateRef);
  }

  private onElse(): void {
    if (!this.appIfFeatureFlagElse) {
      return;
    }

    this.createView(this.appIfFeatureFlagElse);
  }

  private createView(templateRef: TemplateRef<unknown>): void {
    this.viewContainerRef.createEmbeddedView(templateRef);
  }
Enter fullscreen mode Exit fullscreen mode

What happens here is pretty straightforward. When the component is initialized, the directive fetches the feature flag's value and depending on the result calls either onIf method that creates the view of the provided templateRef or onElse method that checks if the fallback template was provided to create an alternative view.

The createView method just calls the relevant method of the viewContainerRef to create a view from the provided templateRef.

Preparing dependencies

For our feature flag directive to work we need a way to get the value of the feature flag. Let's create an interface FeatureFlagsService and an injection token FEATURE_FLAGS_SERVICE with this interface passed as a generic. The interface will contain a single method getFeatureFlag returning a Promise that should resolve to a boolean value.

export interface FeatureFlagsService {
  getFeatureFlag(flagName: string): Promise<boolean>;
}

export const FEATURE_FLAGS_SERVICE = new InjectionToken<FeatureFlagsService>('feature.flags.service');
Enter fullscreen mode Exit fullscreen mode

Why not just create a FeatureFlagService?

We want our directive to depend on abstraction instead of implementation.

This allows us to easily use different FEATURE_FLAGS_SERVICE implementations behind this token depending on the context. For example, we might decide to switch to a different feature flags provider in the future or use a local stub service when using this directive in tests.

Directive in action

Let's test our directive in battle. For this demo purpose, we will use a stub for the FeatureFlagsService service implementation that will return true for FEATURE_1 and false for FEATURE_2:

const FEATURE_FLAGS_MOCK: Record<string, boolean> = {
  FEATURE_1: true,
  FEATURE_2: false
}

@Injectable({providedIn: 'root'})
export class FeatureFlagServiceMock implements FeatureFlagsService {
  public getFeatureFlag(featureFlag: string): Promise<boolean> {
    return Promise.resolve(FEATURE_FLAGS_MOCK[featureFlag]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's import the directive. Since it's a standalone directive, we can import it both in Angular modules and in standalone entities.

  imports: [FeatureFlagDirective]
Enter fullscreen mode Exit fullscreen mode

And use one in the template:

  <div *appIfFeatureFlag="'FEATURE_1'; else: defaultTemplate">Feature 1 template</div>
  <div *appIfFeatureFlag="'FEATURE_2'; else: defaultTemplate">Feature 2 template</div>
  <ng-template #defaultTemplate>Default template</ng-template>
Enter fullscreen mode Exit fullscreen mode

This gives us the following result:

Feature 1 template
Default template
Enter fullscreen mode Exit fullscreen mode

As you see, our conditional display logic becomes declarative and very concise.

Possible enhancements

The directive we built is deliberately simple. But depending on your application's needs you might want to support additional features like:

  • Flag values different from true/false, e.g. strings.
  • Sub-properties of feature flags (sometimes called feature variables in third-party services).
  • Reacting to appIfFeatureFlag changes instead of running the check once in ngOnInit lifecycle hook. It will make your directive more universal, allowing you to pass the feature flag's name dynamically if there is a use case for this.

Keep in mind that reasonable default values are your friends when adding additional options.

Links

You can find the complete version of the code by this link.

Read more about structural directives in the official docs.

Wrapping up

I hope that the practices described in this read and the results we achieved will motivate you to delegate more work to structural directives. If you haven't been doing that already in your Angular projects:)

Thanks for reading and see you in future articles!

Top comments (3)

Collapse
 
sebalr profile image
Sebastian Larrieu • Edited

Nice article, I wrote a similar one a couple of years ago and I created a npm library for this.

Article

NPM library

Collapse
 
dzhavat profile image
Dzhavat Ushev

Nice post. Thanks for writing it.

Collapse
 
vados__bba3f65bec2b12f642 profile image
vados

nice👌