Experiment: Using A Feature Flag To Conditionally Render Routable Components In Angular 9.0.0-next.8
As I've mentioned before, I love using feature flags. They have truly revolutionized the way that me and my team deploy changes to production. That said, on the front-end of our application, I've only used feature flags to enable or disable portions of an existing User Interface (UI) - typically hiding or showing the entry-point to a feature. One thing that I've never done is use a feature flag to completely change the way an Angular Route is rendered. So, as an experiment, I wanted to see if I could use the Router
and the ComponentFactoryResolver
to dynamically and conditionally render a Route Segment based on a Feature Flag setting.
CAUTION: I've never used the
ComponentFactoryResolver
before. As such, please consume the following as a titillating experiment, not as a record of best practices.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
Going into this experiment, I had two goals:
I wanted the URL routes to remain the same regardless of which View Component was being rendered. This was important because I didn't want existing URLs to break due to the feature flag setting. I also wanted the URLs to remain constant if-and-when the old View was permanently replaced by the new View.
I did not want the existing View implementation to know about the alternate View implementation. Keeping the two views separate would make the new code easy to delete if the experiment was a failure, which is an important development quality for me.
Given these constraints / goals, the approach that I came up with was to use an intermediary component that renders the target View based on the feature flag. This intermediary component is inspired by the <router-outlet>
directive; and, renders the target component as a sibling of both the <router-outlet>
and the intermediary component. This way, the DOM (Document Object Model) structure remains constant from an Ancestor / Descendant point-of-view, changing only the number of child elements contained within the current Element node.
This intermediary component then replaces the main View as the routable component in the Router
. So, given the demo context of a Projects List, imagine that we now have three components involved in this dynamic component rendering:
ProjectsComponent
- the existing routable View in the application.ProjectsAltComponent
- the new and exiting alternate View implementation.ProjectsSwitcherComponent
- the intermediary component that renders the appropriate View component based on the feature flag.
Now, in the Router
, the ProjectsSwitcherComponent
replaces the ProjectsComponent
as routable component:
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { ProjectDetailComponent } from "./project-detail.component";
import { ProjectsAltComponent } from "./projects-alt.component";
import { ProjectsComponent } from "./projects.component";
import { ProjectsSwitcherComponent } from "./projects-switcher.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(
[
{
path: "",
pathMatch: "full",
redirectTo: "app"
},
{
path: "app",
children: [
{
path: "projects",
// Normally, the "projects" path would load the existing
// ProjectsComponent; however, imagine that we are currently
// testing an alternate implementation of the design behind a
// feature flag. In order to keep the Routes the same across
// the experiment, we are going to use a "switcher" component
// to act as a proxy that conditionally and dynamically loads
// the appropriate version depending on the feature flag.
// --
// NOTE: The Projects Switcher will dynamically load either
// the ProjectsComponent or the ProjectsAltComponent as a
// "sibling" DOM element, just like the RouterOutlet does.
component: ProjectsSwitcherComponent,
children: [
{
path: ":projectID",
component: ProjectDetailComponent
}
]
}
]
}
],
{
// Tell the router to use the hash instead of HTML5 pushstate.
useHash: true,
// Allow ActivatedRoute to inherit params from parent segments. This
// will force params to be uniquely named, which will help with debugging
// and maintenance of the app.
paramsInheritanceStrategy: "always",
// Enable the Angular 6+ router features for scrolling and anchors.
scrollPositionRestoration: "enabled",
anchorScrolling: "enabled",
enableTracing: false
}
)
],
providers: [],
declarations: [
AppComponent,
ProjectDetailComponent,
// CAUTION: In all the demos (and the documentation) that I've seen about the
// ComponentFactoryResolver, they always include the dynamic components as
// "entryComponents"; however, that did not seem to work for me. For reasons I
// don't fully understand, including the dynamic components as "declarations"
// was sufficient to get this working.
ProjectsComponent,
ProjectsAltComponent
],
bootstrap: [
AppComponent
]
})
export class AppModule {
// ...
}
Notice that the route, /app/projects
, renders the ProjectsSwitcherComponent
. This keeps all of the existing routes in tact, but changes the rendering of the component tree.
ASIDE: When using the
ComponentFactoryResolver
, all of the demos and documentation on the matter seem to indicate that you are supposed to useentryComponents
with this approach. However, that did not work for me and I had to move theProjectsComponent
and theProjectsAltComponent
to thedeclarations
configuration. Maybe this is an Angular 9 change? I am not entirely sure.
The ProjectsSwitcherComponent
then uses the ComponentFactoryResolver
to dynamically render either the ProjectsComponent
or the ProjectsAltComponent
based on the current value of a feature flag:
// Import the core angular services.
import { Component } from "@angular/core";
import { ComponentFactoryResolver } from "@angular/core";
import { ViewContainerRef } from "@angular/core";
// Import the application components and services.
import { ProjectsAltComponent } from "./projects-alt.component";
import { ProjectsComponent } from "./projects.component";
import { UserConfigService } from "./user-config.service";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "app-projects-switcher",
styles: [ `:host { display: none ; }` ],
template:
`
<!-- Switcher for Projects variations. -->
`
})
export class ProjectsSwitcherComponent {
private componentFactoryResolver: ComponentFactoryResolver;
private userConfigService: UserConfigService;
private viewContainerRef: ViewContainerRef;
// I initialize the switcher component.
// --
// NOTE: The injected ViewContainerRef is the container that THIS COMPONENT is
// rendered WITHIN - it is NOT the view for this component's contents.
constructor(
componentFactoryResolver: ComponentFactoryResolver,
userConfigService: UserConfigService,
viewContainerRef: ViewContainerRef
) {
this.componentFactoryResolver = componentFactoryResolver;
this.userConfigService = userConfigService;
this.viewContainerRef = viewContainerRef;
}
// ---
// PUBLIC METHODS.
// ---
// I get called once after the inputs have been bound for the first time.
public ngOnInit() : void {
// Imagine that the UserConfigService holds the feature-flag that drives the
// version of the Projects List that the user is going to see. In order to load
// the selected component dynamically, we're going to use the Component Factory
// Resolver and then load the selected component into the ViewContainerRef as a
// SIBLING element to the Switcher (this) component (just like the RouterOutlet
// directive does).
var factory = ( this.userConfigService.isUsingNewHawtness )
? this.componentFactoryResolver.resolveComponentFactory( ProjectsAltComponent )
: this.componentFactoryResolver.resolveComponentFactory( ProjectsComponent )
;
// Insert as a SIBLING element.
this.viewContainerRef.createComponent( factory );
}
}
For the purposes of this demo, the UserConfigService
holds the value of our feature flag, isUsingNewHawtness
. When this feature flag is off, the existing ProjectsComponent
is rendered; and, when this feature flag is on, the new and experimental ProjectsAltComponent
is rendered. In either case, the selected component is rendered as a sibling to the intermediary component, which is, in turn, rendered as a sibling to the <router-outlet>
component.
Now, since I don't have a true feature flag system available for this demo, I am simulating the feature flag with a simple toggle in the App component. In the following code, notice that I am doing nothing more than manually setting the isUsingNewHawtness
property on the injected UserConfigService
:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { UserConfigService } from "./user-config.service";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "app-root",
styleUrls: [ "./app.component.less" ],
template:
`
<p class="flag-controls">
<strong>Set Feature Flag</strong>:
<a (click)="setFeatureFlag( true )">Yes</a> ,
<a (click)="setFeatureFlag( false )">No</a>
—
( Current: <strong>{{ userConfigService.isUsingNewHawtness }}</strong> )
</p>
<nav>
<a routerLink="/app">Home</a> ,
<a routerLink="/app/projects">Projects</a>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
public userConfigService: UserConfigService;
// I initialize the app component.
constructor( userConfigService: UserConfigService ) {
this.userConfigService = userConfigService;
}
// ---
// PUBLIC METHODS.
// ---
// I set the feature flag that determines which version of the Projects list is
// rendered on the "projects" route.
// --
// NOTE: This is just for the demo. Normally, a feature flag would be configured by
// the Product Team based on targeting rules.
public setFeatureFlag( value: boolean ) : void {
this.userConfigService.isUsingNewHawtness = value;
}
}
At this point, we have our simulated feature flag, a means to toggle it on-and-off, an intermediary View component wired into the Router
, and an alternate implementation of the Projects List. If we run this Angular 9 application, we can see that the Projects List View component can be swapped out without changing any existing routes:
As you can see, when the feature flag is enabled, the alternate implementation of the Projects List View is rendered in the application. But, all of the existing URL routes remain constant. At the DOM-level, this dynamic component rendering is structured like this:
Because of the intermediary component, there is an extra Element in the DOM; but, notice that it is at the same level as the <router-outlet>
; and, that it keeps the dynamically-rendered Angular component at the same level as well. This keeps the DOM changes down to a minimum, maintaining the same fundamental shape of the DOM Tree.
There are a few other View components in this code; but, they aren't really relevant to the exploration. As such, I'll leave them in the linked code repository and omit them from this write-up.
This is the first time that I've ever looked at rendering a dynamic component in Angular 2+. So, hopefully I haven't missed anything too critical. That said, I do love the fact that I can now use a Feature Flag to swap out a View component implementation in the Angular 9 Router without having to disrupt any of the existing URL routes. This will make larger changes much easier to slide into the existing user experience!
Want to use code from this post? Check out the license.
Reader Comments
Note to self: I should write some sort of post about how I consume feature flags in general, in Angular.
Hi!, good article!,
I read about:
// CAUTION: In all the demos (and the documentation) that I've seen about the
// ComponentFactoryResolver, they always include the dynamic components as
// "entryComponents"; however, that did not seem to work for me. For reasons I
// don't fully understand, including the dynamic components as "declarations"
// was sufficient to get this working.
Just by informastion, the entryComponents is deprecated in angular 9, for that reason I think it doesnt work for you. :)
thanks!