Created
September 27, 2019 12:07
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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