Angular Feature Flags
Usage
*ifFeatureFlag
Show the control if the feature flag is enabled.
<div *ifFeatureFlag="'feature-one-trial:control'">
<button>click me (feature-one-trial:control)</button>
</div>
*ifFeatureFlag
Show the control if the set of feature flag is enabled. Specify the operator to define how to perform the condition:
- AND all the feature flags need to be available to show the control
- OR if one of the feature flags is available, show the control
<div *ifFeatureFlags="['feature-two-trial:treatment','feature-one-trial:control']; operator: 'AND'">
<button>click me (IF 'feature-two-trial:treatment','feature-one-trial:control' AND)</button>
</div>
*ifNotFeatureFlag
Show the control if the feature flag is not enabled.
<div *ifNotFeatureFlag="'feature-one-trial:treatment'">
<button>click me (not in feature-one-trial:treatment)</button>
</div>
*ifNotFeatureFlags
Show the control if the set of feature flag is enabled. Specify the operator to define how to perform the condition:
- AND all the feature flags need to be available to hide the control
- OR if one of the feature flags is available, hide the control
<div *ifNotFeatureFlags="['feature-two-trial:treatment','feature-one-trial:control']; operator: 'AND'">
<button>click me (IF 'feature-two-trial:treatment','feature-one-trial:control' AND)</button>
</div>
featureFlagTrace
When a click event is recieved push it to the monitor observable.
<div *ifFeatureFlag="'feature-one-trial:control'">
<button (click)="title='clicked'" featureFlagTrace="feature-one-trial:control|ok">click me</button>
<button (click)="title='clicked'" featureFlagTrace="feature-one-trial:control|fail">don't click me</button>
</div>
Access the feature observable from code
If you need the observable in code, you can either use the featureFlagService.featureFlags$
full observable collection, or get it prefiltered by using featureFlagService.featureOn$
or featureFlagService.featureOff$
to recieve a boolean observable.
import { Component } from '@angular/core';
import { FeatureFlagsService } from '@eriklieben/angular-feature-flags';
import { Observable } from 'rxjs';
@Component({
selector: 'app-comp-two',
templateUrl: './comp-two.component.html',
styleUrls: ['./comp-two.component.scss']
})
export class CompTwoComponent {
public features$: Observable<Set<string>>;
public featureOn$: Observable<boolean>;
constructor(
featureFlagService: FeatureFlagsService) {
this.features$ = featureFlagService.featureFlags$;
this.featureOn$ = featureFlagService.featureOn$('feature-one-trail:control');
}
}
<ul *ngIf="(features$ | async) as features">
<li *ngFor="let feature of features">
{{feature}}
</li>
</ul>
Perform a one-time check.
import { Component } from '@angular/core';
import { FeatureFlagsService } from '@eriklieben/angular-feature-flags';
@Component({
selector: 'app-comp-two',
templateUrl: './comp-two.component.html',
styleUrls: ['./comp-two.component.scss']
})
export class CompTwoComponent {
public isEnabled = false;
public isDisabled = true;
constructor(featureFlagService: FeatureFlagsService) {
this.isEnabled = featureFlagService.featureOn('feature-one-trail:control');
this.isDisabled = featureFlagService.featureOff('feature-one-trail:control');
}
}
Setup
Install the package npm i @eriklieben/angular-feature-flags -S
and configure a FeatureFlagStore
to retrieve feature toggles.
In the sample below we assume you recieve the following data from the server when calling /api/getfeaturetoggles
:
{
"featureFlags": {
"feature-one-trial" : "control",
"feature-two-trial" : "treatment",
"feature-source": "app" // only here for demonstration
}
}
Configure your store to map the data to a list of strings for your features.
@Injectable({
providedIn: 'root',
})
export class AppFeatureStore implements FeatureFlagStore {
featureFlags$: Observable<string[]>;
constructor(httpClient: HttpClient) {
this.featureFlags$ = httpClient.get<FeatureToggleData>('/api/getfeaturetoggles')
.pipe(map((val) => {
const flags: string[] = [];
const featureFlags = Object.keys(val.featureFlags);
for(let flag of featureFlags) {
flags.push(`${flag}:${val.featureFlags[flag]}`);
}
return flags;
}),
shareReplay(1));
}
}
interface FeatureToggleData {
featureFlags: {[key: string]: any};
}
Add the feature store implementation to your providers sections:
@NgModule({
declarations: [
AppComponent
],
imports: [
HttpClientModule,
BrowserModule,
AppRoutingModule,
FeatureFlagsModule
],
providers: [
{
provide: FeatureFlagStore,
useClass: AppFeatureStore
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Combining app and lazy loaded module feature flags
We assume the following comes back from the backend call to /api/module-two/features
:
{
"featureFlags": {
"feature-five-trial" : "control",
"feature-six-trial" : "treatment",
"feature-module" : "two",
},
}
And we will combine it with the data from /api/getfeaturetoggles
retrieved in the AppModule.
@Injectable()
export class ModuleTwoFeatureStore implements FeatureFlagStore {
featureFlags$: Observable<string[]>;
constructor(httpClient: HttpClient, appFeatureStore: AppFeatureStore) {
const retrieveLazyModuleFeatureFlags = httpClient.get<FeatureToggleData>('/api/module-two/features')
.pipe(
map((val) => {
const flags: string[] = [];
const featureFlags = Object.keys(val.featureFlags);
for(let flag of featureFlags) {
flags.push(`${flag}:${val.featureFlags[flag]}`);
}
return flags;
}));
this.featureFlags$ = forkJoin([
retrieveLazyModuleFeatureFlags,
appFeatureStore.featureFlags$
]).pipe(
map(([a,b]) => [...a,...b]),
shareReplay(1));
}
}
And our configuration for the module:
@NgModule({
declarations: [
CompTwoComponent
],
imports: [
CommonModule,
FeatureFlagsModule,
ModuleTwoRoutingModule
],
exports: [
CompTwoComponent
],
providers: [
{
provide: FeatureFlagStore,
useClass: ModuleTwoFeatureStore
}
]
})
export class ModuleTwoModule { }
Using a different dataset for a component and all it's childs
We assume the server returns the following:
{
"featureFlags": {
"feature-one-trial" : "control",
"feature-two-trial" : "treatment",
"feature-module" : "parent",
},
}
import { HttpClient } from '@angular/common/http';
import { Component, Injectable } from '@angular/core';
import { FeatureFlagStore } from '@eriklieben/angular-feature-flags';
import { map, Observable, shareReplay } from 'rxjs';
@Injectable()
export class ParentComponentFeatureStore implements FeatureFlagStore {
featureFlags$: Observable<string[]>;
constructor(httpClient: HttpClient) {
this.featureFlags$ = httpClient.get<FeatureToggleData>('/api/parent-component/features')
.pipe(map((val) => {
const flags: string[] = [];
const featureFlags = Object.keys(val.featureFlags);
for(let flag of featureFlags) {
flags.push(`${flag}:${val.featureFlags[flag]}`);
}
return flags;
}),
shareReplay(1));
}
}
interface FeatureToggleData {
featureFlags: {[key: string]: any};
}
In the component decorator we provide the store with the observable that provides the feature flags for the given component and all it's children (if not overridden again).
@Component({
selector: 'app-parent-component-one',
templateUrl: './parent-component-one.component.html',
styleUrls: ['./parent-component-one.component.scss'],
providers: [
{
provide: FeatureFlagStore,
useClass: ParentComponentFeatureStore
}
]
})
export class ParentComponentOneComponent {
}