Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created September 27, 2019 12:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/4687302622baa01cc6204310477cf7f4 to your computer and use it in GitHub Desktop.
Save bennadel/4687302622baa01cc6204310477cf7f4 to your computer and use it in GitHub Desktop.
Experiment: Using A Feature Flag To Conditionally Render Routable Components In Angular 9.0.0-next.8
// 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>
&mdash;
( 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;
}
}
// 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 {
// ...
}
// 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 );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment