The problem
In one of the projects I'm working on we wanted to show/hide certain elements/features at runtime.
The requirements were pretty simple:
- Manually enable and disable the toggle in a configuration file
- It should be possible to change the value at runtime
Solution
A well known solution for this are Feature Flags or Feature Toggles. A lot has already been written about this topic e.g. you can find some details here.
There are quite some ready made solutions out there. However most of the available solutions are SaaS offerings and these are not always an option (for technical or financial reasons).
So, as we don't have many (complicated) requirements, it shouldn't be too hard to build our own, local version.
Overview
The following diagram gives an overview of the main elements
As mentioned previously, we are aiming for a rather simple solution so there is not too much going on.
Implementation
Store
All of the configuration is stored in the feature.json
file. It has a pretty simple structure with the key being the name of the feature and the value a boolean which indicates if the feature is enabled or not.
This is enough for basic use cases. However if there are a lot flags it could make sense to add bit more structure here e.g. nesting the different flags under a key which represents the pages or components they are associated with.
It is also possible to define different feature.json
files as a sort of template for a specific configuration e.g. if the application is deployed for multiple customers.
{
"newFeature": true
}
FeatureService
Bootstrapping
The root injected service loads the feature.json
file and stores it in memory.
The file could be validated and typed after loading (e.g. using something like zod).
I personally did not see any advantage in doing that as the file is only used in the isFeatureEnabled
method which checks if the provided key is truthy. Having it typed would not give any benefit and has the drawback that the validation and type definition has to be updated whenever a new property is added to the feature.json
file.
init(): Observable<void> {
const result = new ReplaySubject<void>(1);
this.http
.get<typeof this.featuresFlags>('assets/feature.json')
.pipe(
first(),
catchError(() => of({}))
)
.subscribe((featureFlags) => {
this.featuresFlags = featureFlags;
result.next();
result.complete();
});
return result;
}
For the sake of the example the loading mechanismn is kept pretty simple (e.g. it does not include any error handling, it hard codes the path...).
The init method is called by an APP_INITIALIZER
at application start up.
It returns an Observable
to make sure the first render does not happen before the feature.json
file has been loaded.
const initFeatureServiceFactory = (featureService: FeatureService) => {
return () => featureService.init();
};
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
WithoutDirectiveComponent,
WithDirectiveComponent,
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initFeatureServiceFactory,
deps: [FeatureService],
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
IsFeatureEnabled
The aformentioned isFeatureEnabled
method returns if the feature is enabled or not.
The implementation is pretty straight forward at the moment:
isFeatureEnabled(name: string): boolean {
return this.featuresFlags[name];
}
Having a dedicated function hides the storage mechanism and allows for changing to a different approach in the future with minor impact on the using components.
Components
For demonstration purposes two components have been added to the project
WithoutDirectiveComponent
As the name implies the component does the feature check for itself by directly calling the isFeatureEnabled
with the feature name.
export class WithoutDirectiveComponent {
isNewFeatureEnabled = false;
constructor(public featureService: FeatureService) {
this.isNewFeatureEnabled = this.featureService.isFeatureEnabled('newFeature');
}
<p *ngIf="isNewFeatureEnabled; else featureDisabled">
WithoutDirectiveComponent: Feauture enabled
</p>
<ng-template #featureDisabled>
<p>WithoutDirectiveComponent: Feature disabled</p>
</ng-template>
FeatureDirective
Repeating the "check and render" is a bit tedious and identical for every using component. Therefore a directive has been added to the project that takes care of this part. It's working similar to ngIf
but takes the feature name instead of a boolean
.
The directive passes the provided feature name to the FeatureService
and leverages the ngIf
directive for hiding or showing the feature flagged element.
Similar to ngIf
it also allows for displaying an alternative template
should the feature be disabled.
@Directive({
selector: '[appIfFeatureEnabled]',
standalone: true,
hostDirectives: [
{
directive: NgIf,
inputs: ['ngIfElse: appIfFeatureEnabledElse'],
},
],
})
export class IfFeatureEnabledDirective {
@Input() set appIfFeatureEnabled(appIfFeatureEnabled: string) {
this.ngIf.ngIf = this.featureService.isFeatureEnabled(
appIfFeatureEnabled ?? false
);
}
constructor(private ngIf: NgIf, private featureService: FeatureService) {}
}
WithDirectiveComponent
The component just needs to pass to feature name to the FeatureDirective
<p *appIfFeatureEnabled="'newFeature'; else featureDisabled">
WithDirectiveComponent: Feature enabled
</p>
<ng-template #featureDisabled
>WithDirectiveComponent: Feature disabled</ng-template
>
Conclusion
This is it, a simple solution for feature flagging templates elements in Angular.
As mentioned in the beginning, the implementation only covers simple hide/show use cases which can often be enough, especially in the beginning.
In our project, it has served us as well so far.
It was easy and fast to build, it's easy to maintain (e.g. adding new feature flags) and avoids adding another dependency to a third party library (or company) and, most importantly, it fulfils all of our current requirements.
The code can be found here
Top comments (0)