Why a custom structural directive for feature flags?

Custom structural directives unlock a declarative style of writing templates and help keep components tidy and free from repeated boilerplate. In this walkthrough, we'll put together a compact structural directive that toggles between two template fragments based on a feature flag's value. The public surface of the directive is intentionally small:

  • The directive conditionally renders template pieces depending on whether the flag is enabled.
  • The component class stays untouched—just importing the directive makes it usable in the template.

What makes this version stand out

Feature-flag directives are a familiar sight in many Angular apps. But with a few extra tricks, we can create something more flexible by supporting a fallback template for when the flag evaluates to false. This option is handy and yet, in my experience across several Angular codebases, far less common than it should be.

If you've written *ngIf=A; else B before, you already know the pattern. Here's how our directive will appear in a component template:

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

Along the way, we'll lean on some relatively new Angular features—standalone directives and the inject function, which replaces constructor-based injection.

Why feature flags

We could have picked any shared feature to demonstrate a structural directive. But let me briefly explain why feature flags are a fitting example.

Building resilient applications takes effort: strong test suites, sound abstractions, quick iteration cycles, and more. Feature flags, by contrast, are quick to introduce and don't require overhauling your architecture. They don't replace the practices above, but they're a sensible starting point. With a flag, you can instantly disable a problematic change in any environment without a redeployment.

In my view, feature flags are non-negotiable for any production application.

That's a win for users, who aren't stuck with broken functionality while waiting for a fix, and for developers, who get breathing room to solve the issue calmly.

Implementing the directive

Let's dive in. We'll first define 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);

  //...
}

The directive is marked standalone: true so it can be imported without an NgModule. We declare two inputs: one for the feature flag's name, and another for the alternative template to display when the flag is false. Since the code is written in TypeScript strict mode, we make optional and required inputs explicit with the usual assertion operators.

Take note of the input names.

To get the template syntax we want, all additional inputs must be prefixed with the directive's selector, which is appIfFeatureFlag here.

So for the fallback ng-template used with the else parameter, the input is named appIfFeatureFlagElse.

Remember how the camelCase suffix appears in the template as a lowercase binding:

<div *appIfFeatureFlag="'FEATURE_1'; else: defaultTemplate">Feature</div>

Next, we inject the following:

  • templateRef — the reference to the template we want to render when the flag is on, i.e., the content inside the element carrying *appIfFeatureFlag.

If we dropped the leading *, we'd get a No provider for TemplateRef found error. There's nothing in the @Directive decorator stating this is a structural directive rather than an attribute one. The * is syntactic sugar that expands the host element into this wrapper construct:

<ng-template [appIfFeatureFlag]="'FEATURE_1'">
  <div>Feature</div>
</ng-template>

The directive also relies on:

  • viewContainerRef — the container responsible for creating embedded views inside the host element.

  • featureFlagService — the service that provides access to feature flag values. We'll touch on good practices for such dependencies shortly.

Since Angular 14, these can be acquired without going through the constructor, using the inject function which is callable during construction.

Now for 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);
  }

The flow is simple. On initialization, the directive checks the flag's value and, depending on the outcome, invokes either onIf to render the provided templateRef or onElse to render the fallback template if one was supplied.

The createView method simply calls the appropriate viewContainerRef method to produce a view from the given templateRef.

Setting up dependencies

For the directive to function, we need a source for flag values. Let's define an interface FeatureFlagsService and an injection token FEATURE_FLAGS_SERVICE typed with that interface. The interface exposes a single method getFeatureFlag that returns a Promise resolving to a boolean.

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

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

Why an interface and a token instead of a concrete class?

The goal is to have the directive depend on an abstraction, not a particular implementation.

This makes it easy to swap in different FEATURE_FLAGS_SERVICE providers depending on the context—say, switching to another feature flag vendor later, or using a lightweight stub in tests.

Using the directive

Let's see it in action. For this demo, we'll use a stub implementation of FeatureFlagsService that returns 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]);
  }
}

Next, import the directive. Being standalone, it can be brought into module-based and standalone components alike.

  imports: [FeatureFlagDirective]

And finally, use it in a 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>

This produces:

Feature 1 template
Default template

As you can see, the conditional rendering logic becomes clean and fully declarative.

Ideas for extending the directive

The directive we built is intentionally minimal. Depending on your project, you might want to add:

  • Support for flag values other than true/false, like strings.
  • Nested flag properties (sometimes called feature variables in third-party tools).
  • Listening to changes on appIfFeatureFlag rather than checking once in ngOnInit. That would make the directive more versatile, letting you pass the flag's name dynamically when needed.

When adding options, sensible defaults go a long way.

The full source code is available here.

For more on structural directives, check the official documentation.

In closing

I hope the patterns above inspire you to offload more templating work to structural directives, even if you haven't used them much in your Angular projects yet.

Thanks for reading, and see you next time!