Hi everyone, in this tutorial we will create and publish a NPM library for Angular. In this case, we are going to use services, directives and the Angular injector to build a feature flag library that will allow us to show (or hide) features based on conditions, like a custom *ngIf.
Lets start with the Angular library skeleton, it will allow us to create and test the library. Using CLI:
// You could change prefix or don't use parameter, default prefix is app
ng new ngx-flags --prefix=slr
cd ngx-flags
ng generate library ngx-feature-flags
Now we have a src folder where we will test and use the library (as in a real Angular project) and a project folder where we will write the actual library code. You could delete component.ts and .spec.ts in project/src/lib as we don't need them.
Lets write the service, it will hold the features list and be consulted by directives to check if a feature is set to on.
We need a Map to maintain de features and a bool to check if we have already initialised it.
private featureFlags: Map<string, boolean>;
private initialized = false;
get Initialized() {
return this.initialized;
}
As we are building a library, we don't know how people will set their features so we will inject a lambda function in constructor for the initialisation action. I will talk about this later. We also need a refresh token to update the view when a new Map arrives.
private refresh: Subject<boolean>;
public refresh$: Observable<boolean>;
constructor(private initFun: () => Promise<Map<string, boolean>>) {
this.featureFlags = new Map();
this.refresh = new Subject();
this.refresh$ = this.refresh.asObservable();
}
As I said, we will use that function to get the features
public async initialize() {
this.featureFlags.clear();
this.featureFlags = await this.initFun();
this.initialized = true;
this.refresh.next(true);
}
Now the actual work, we need functions to detect and check for a specific feature. We will have both positive and negative because we will probably need to show one section of our app if a feature is enabled or another if its not (like a if-else)
public featureOff(featureName: string) {
return !this.featureOn(featureName);
}
public featureOn(featureName: string) {
if (!featureName) {
return true;
}
return this.featureFlags.has(featureName) && is.featureFlags.get(featureName);
}
Now we are going to build two structural directives to show elements based on a feature name (you could read more about structural directives here, the basic information you need right now its that when we use structural directives (with an * like *ngIf) Angular allow us to inject the host element to manipulate it. So inside the lib folder of the library project run
ng g directive showIfFeature
ng g directive showIfNotFeature
I will show one of them, as are both equivalent. We will need a private nameFeature property and an Input setter so we can react to changes, if you don't like this approach, you could use onChage lifeCycle hook. As we are building a structural directive to manipulate DOM, we need the TemplateRef (holds a reference to its host element) and ViewContainerRef (a container where we can attach views). We will use the refresh token to update the view when the service told us that the features are available. We will also need the service we have just write so lets inject all the things.
private featureName: string;
@Input() set ngxShowIfFeature(feature: string) {
this.featureName = feature;
this.showOrHide();
}
private subs: Subscription;
constructor(private featureFlagService: NgxFeatureFlagsService, private templateRef: TemplateRef<any>, private vcr: ViewContainerRef) {}
ngOnInit() {
this.subs = new Subscription();
this.subs.add(this.featureFlagService.refresh$.subscribe(() => this.showOrHide()));
}
//Never forget to unsubscribe to avoid memory leaks
ngOnDestroy() {
this.subs.unsubscribe();
}
With all in place, the showOrHide is very simple. We check the condition and put the view (the templateRef) in DOM using the vcr.
private showOrHide() {
if (this.featureName) {
const featureOn = this.featureFlagService.featureOn(this.featureName);
if (featureOn && !this.hasView) {
this.vcr.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (!featureOn && this.hasView) {
this.vcr.clear();
this.hasView = false;
}
}
}
Now lets build a guard so we could also control navigation based on our feature service. Lets use CLI (check both canActivate and canActivateChild).
ng g guard featureFlags
Now we have to add a constructor and change canActivate to use our feature service. We will use the data property to tell our guard wich feature are we checking
constructor(private featureFlagService: FeatureFlagService) { }
async canActivate(route: ActivatedRouteSnapshot) {
const featureFlag = route.data.featureFlag;
if (!featureFlag) {
return true;
}
if (this.featureFlagService.Initialized) {
return this.featureFlagService.featureOn(featureFlag);
} else {
await this.featureFlagService.initialize();
return this.featureFlagService.featureOn(featureFlag);
}
}
The canActivateChild() will reuse the canActivate.
async canActivateChild(route: ActivatedRouteSnapshot) {
return await this.canActivate(route);
}
We are almost done with coding. Remember to add directives to export array in the library module. Also remove the injectable part from the service. Don't worry, I will talk about this in next steps.
// remove this
import { Injectable } from '@angular/core';
// and this
@Injectable({
providedIn: 'root'
})
We are almost there, now we can compile the libray, in root folder run
ng build ngx-feature-flags
If everything finish correctly, lets configure and use the library in the application. I forgot to change the selector in library so inside tslint.json change 'lib' for 'ngx' and change directives names.
If you remember, our library need an injected function so it could initialise the features Map. I'm going to write a service so I could get that information from anywhere (maybe hard-coding features at first, and using an API later), that is the best part of using services for abstraction.
So from the root folder use the CLI to create the service in the app
cd src/app
ng g service featuresConfiguration
This is a normal service (so we can inject any other service we need in the constructor in order to get the features of our system). For this tutorial, I'm just going to return a harcoded Map (don't judge me). The function must be a lamda so it will keep context if we use some injected service to get the features (for example, an userService with an API connection method to get permissions from current logged user)
constructor() { }
public getFeatureFlags = async (): Promise<Map<string, boolean>> => {
const flags = new Map<string, boolean>();
flags.set('featureA', true);
flags.set('featureB', false);
return flags;
}
You may be wondering ¿How could I inject that function in the library service?. Well, the answer is factory providers. We will create a provider that will inject that function as the initFun of the flags service.
In the app folder, create a ngx-feature-flags.service.provider.ts. It will have a factory to create the service and a provider section that will use that factory.
import { FeatureFlagProviderService } from './feature-flag-provider.service';
import { FeatureFlagService } from './feature-flags.service';
const featureFlagServiceFactory = (provider: FeatureFlagProviderService) => {
return new FeatureFlagService(provider.getFeatureFlags);
};
export let FeatureFlagServiceProvider = {
provide: FeatureFlagService,
useFactory: featureFlagServiceFactory,
deps: [FeaturesConfigurationService]
};
If the factory need some services, for example the FeatureFlagProviderService, you have to add them in the deps array of the provider. Now in the providers array section of the appModule, you have to add the FeatureFlagServiceProvider and also add the NgxFeatureFlagsModule in the imports array.
Now lets change the app.html to use our new directives
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
</div>
<h2>Here are some links features: </h2>
<ul>
<li *ngxShowIfFeature="'featureA'">
<p>Feature A </p>
</li>
<li *ngxShowIfFeature="'featureB'">
<p>Feature B </p>
</li>
<li *ngxShowIfNotFeature="'featureC'">
<p>Feature C </p>
</li>
</ul>
Lets add some scripts in package.json to test the library
"build-lib": "ng build ngx-feature-flags",
"debug": "npm run build-lib && ng serve"
Now if you run the command and go to localhost:4200, you will only see Feature C, as we didn't enable any feature. To change that, inject the feature service in app component and call the init function.
export class AppComponent {
title = 'ngx-flags';
constructor(private flagService: NgxFeatureFlagsService) {
this.flagService.initialize();
}
}
Now save and go to the browser again for a TA-DA moment!
The library is ready so lets publish it. Lets add a readme and a license files and some more NPM scripts.
"package": "npm run build-lib && npm run copy-files && npm run npm-pack",
"debug": "npm run build-lib && ng serve",
"build-lib": "ng build ngx-feature-flags",
"copy-files": "npm run copy-license && npm run copy-readme",
"copy-license": "cp ./LICENSE ./dist/ngx-feature-flags",
"copy-readme": "cp ./README.md ./dist/ngx-feature-flags",
"npm-pack": "cd dist/ngx-feature-flags && npm pack"
Then you have to package and publish. First run npm login and complete with username, pass and email when asked. Then in root folder run
npm run package
npm publish dist/ngx-feature-flags/ngx-feature-flags-1.0.0.tgz --access public
And its done. If you don't want to publish the library, you just cant install the generated .tgz using npm install path-to-file or use my library :), it's already in NPM.
That's all. I hope you liked it, this is the project github.
Top comments (0)