Skip to content

Commit

Permalink
feat(ng2.UIView): Use merged NgModule/ParentComp to inject routed com…
Browse files Browse the repository at this point in the history
…ponent

feat(ng2.NgModule): Add the root NgModule to root resolve
  • Loading branch information
christopherthielen committed Aug 31, 2016
1 parent c3857b5 commit 37241e7
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 52 deletions.
134 changes: 84 additions & 50 deletions src/ng2/directives/uiView.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
/** @module ng2_directives */ /** */
import {
Component, ComponentFactoryResolver, ComponentFactory,
ViewContainerRef, ReflectiveInjector, InputMetadata, ComponentMetadata, ViewChild
Component, ComponentFactoryResolver, ViewContainerRef, Input, ComponentRef, Type,
ReflectiveInjector, InputMetadata, ComponentMetadata, ViewChild, Injector, Inject
} from '@angular/core';
import {Input} from "@angular/core";
import {ComponentRef} from "@angular/core";
import {Type} from "@angular/core";

import {UIRouter} from "../../router";
import {trace} from "../../common/trace";
import {Inject} from "@angular/core";
import {ViewContext, ViewConfig} from "../../view/interface";
import {Ng2ViewDeclaration} from "../interface";
import {ViewContext, ViewConfig, ActiveUIView} from "../../view/interface";
import {NG2_INJECTOR_TOKEN} from "../interface";
import {Ng2ViewConfig} from "../statebuilders/views";
import {ResolveContext} from "../../resolve/resolveContext";
import {flattenR} from "../../common/common";
import {MergeInjector} from "../mergeInjector";

/** @hidden */
let id = 0;
Expand Down Expand Up @@ -132,14 +129,13 @@ export class UIView {
@Input('ui-view') set _name(val: string) { this.name = val; }
componentRef: ComponentRef<any>;
deregister: Function;
uiViewData: any = {};
uiViewData: ActiveUIView = <any> {};

static PARENT_INJECT = "UIView.PARENT_INJECT";

constructor(
public router: UIRouter,
@Inject(UIView.PARENT_INJECT) public parent: ParentUIViewInject,
public compFactoryResolver: ComponentFactoryResolver,
public viewContainerRef: ViewContainerRef
) { }

Expand Down Expand Up @@ -170,56 +166,94 @@ export class UIView {
this.disposeLast();
}

/**
* The view service is informing us of an updated ViewConfig
* (usually because a transition activated some state and its views)
*/
viewConfigUpdated(config: ViewConfig) {
// The config may be undefined if there is nothing currently targeting this UIView.
// Dispose the current component, if there is one
if (!config) return this.disposeLast();
if (!(config instanceof Ng2ViewConfig)) return;

let uiViewData = this.uiViewData;
let viewDecl = <Ng2ViewDeclaration> config.viewDecl;
// Only care about Ng2 configs
if (!(config instanceof Ng2ViewConfig)) return;

// The "new" viewconfig is already applied, so exit early
if (uiViewData.config === config) return;
// This is a new viewconfig. Destroy the old component
if (this.uiViewData.config === config) return;

// This is a new ViewConfig. Dispose the previous component
this.disposeLast();
trace.traceUIViewConfigUpdated(uiViewData, config && config.viewDecl.$context);
uiViewData.config = config;
// The config may be undefined if there is nothing state currently targeting this UIView.
if (!config) return;
trace.traceUIViewConfigUpdated(this.uiViewData, config && config.viewDecl.$context);

// Map resolves to "useValue providers"
this.applyUpdatedConfig(config);
}

applyUpdatedConfig(config: Ng2ViewConfig) {
this.uiViewData.config = config;
// Create the Injector for the routed component
let context = new ResolveContext(config.path);
let resolvables = context.getTokens().map(token => context.getResolvable(token)).filter(r => r.resolved);
let rawProviders = resolvables.map(r => ({ provide: r.token, useValue: r.data }));
rawProviders.push({ provide: UIView.PARENT_INJECT, useValue: { context: config.viewDecl.$context, fqn: uiViewData.fqn } });
let componentInjector = this.getComponentInjector(context);

// Get the component class from the view declaration. TODO: allow promises?
let componentType = <any> viewDecl.component;

let createComponent = (factory: ComponentFactory<any>) => {
let parentInjector = this.viewContainerRef.injector;
let childInjector = ReflectiveInjector.resolveAndCreate(rawProviders, parentInjector);
let ref = this.componentRef = this.componentTarget.createComponent(factory, undefined, childInjector);

// TODO: wire uiCanExit and uiOnParamsChanged callbacks

let bindings = viewDecl['bindings'] || {};
var addResolvable = (tuple: InputMapping) => ({
prop: tuple.prop,
resolvable: context.getResolvable(bindings[tuple.prop] || tuple.token)
});

// Supply resolve data to matching @Input('prop') or inputs: ['prop']
let inputTuples = ng2ComponentInputs(componentType);
inputTuples.map(addResolvable)
.filter(tuple => tuple.resolvable && tuple.resolvable.resolved)
.forEach(tuple => { ref.instance[tuple.prop] = tuple.resolvable.data });

// Initiate change detection for the newly created component
ref.changeDetectorRef.detectChanges();
};
let componentClass = config.viewDecl.component;

// Create the component
let compFactoryResolver = componentInjector.get(ComponentFactoryResolver);
let compFactory = compFactoryResolver.resolveComponentFactory(componentClass);
this.componentRef = this.componentTarget.createComponent(compFactory, undefined, componentInjector);

// Wire resolves to @Input()s
this.applyInputBindings(this.componentRef, context, componentClass);

let factory = this.compFactoryResolver.resolveComponentFactory(componentType);
createComponent(factory);
// TODO: wire uiCanExit and uiOnParamsChanged callbacks
}
}

/**
* Creates a new Injector for a routed component.
*
* Adds resolve values to the Injector
* Adds providers from the NgModule for the state
* Adds providers from the parent Component in the component tree
* Adds a PARENT_INJECT view context object
*
* @returns an Injector
*/
getComponentInjector(context: ResolveContext): Injector {
// Map resolves to "useValue: providers"
let resolvables = context.getTokens().map(token => context.getResolvable(token)).filter(r => r.resolved);
let newProviders = resolvables.map(r => ({ provide: r.token, useValue: r.data }));

var parentInject = { context: this.uiViewData.config.viewDecl.$context, fqn: this.uiViewData.fqn };
newProviders.push({ provide: UIView.PARENT_INJECT, useValue: parentInject });

let parentComponentInjector = this.viewContainerRef.injector;
let moduleInjector = context.getResolvable(NG2_INJECTOR_TOKEN).data;
let mergedParentInjector = new MergeInjector(moduleInjector, parentComponentInjector);

return ReflectiveInjector.resolveAndCreate(newProviders, mergedParentInjector);
}

/**
* Supplies component inputs with resolve data
*
* Finds component inputs which match resolves (by name) and sets the input value
* to the resolve data.
*/
applyInputBindings(ref: ComponentRef<any>, context: ResolveContext, componentClass) {
let bindings = this.uiViewData.config.viewDecl['bindings'] || {};

var addResolvable = (tuple: InputMapping) => ({
prop: tuple.prop,
resolvable: context.getResolvable(bindings[tuple.prop] || tuple.token)
});

// Supply resolve data to matching @Input('prop') or inputs: ['prop']
let inputTuples = ng2ComponentInputs(componentClass);
inputTuples.map(addResolvable)
.filter(tuple => tuple.resolvable && tuple.resolvable.resolved)
.forEach(tuple => { ref.instance[tuple.prop] = tuple.resolvable.data });

// Initiate change detection for the newly created component
ref.changeDetectorRef.detectChanges();
}
}
2 changes: 2 additions & 0 deletions src/ng2/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,5 @@ export interface Ng2Component {
*/
uiCanExit(): HookResult;
}

export const NG2_INJECTOR_TOKEN = {};
22 changes: 22 additions & 0 deletions src/ng2/mergeInjector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Injector} from "@angular/core";

export class MergeInjector implements Injector {
static NOT_FOUND = {};
private injectors: Injector[];
constructor(...injectors: Injector[]) {
if (injectors.length < 2) throw new Error("pass at least two injectors");
this.injectors = injectors;
}

get(token: any, notFoundValue?: any): any {
for (let i = 0; i < this.injectors.length; i++) {
let val = this.injectors[i].get(token, MergeInjector.NOT_FOUND);
if (val !== MergeInjector.NOT_FOUND) return val;
}

if (arguments.length >= 2) return notFoundValue;

// This will throw the DI Injector error
this.injectors[0].get(token);
}
}
6 changes: 5 additions & 1 deletion src/ng2/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ import {UrlRouter} from "../url/urlRouter";
import {ViewService} from "../view/view";
import {UIView, ParentUIViewInject} from "./directives/uiView";
import {ng2ViewsBuilder, Ng2ViewConfig} from "./statebuilders/views";
import {Ng2ViewDeclaration} from "./interface";
import {Ng2ViewDeclaration, NG2_INJECTOR_TOKEN} from "./interface";
import {UIRouterConfig} from "./uiRouterConfig";
import {Globals} from "../globals";
import {UIRouterLocation} from "./location";
import {services} from "../common/coreservices";
import {ProviderLike} from "../state/interface";
import {Resolvable} from "../resolve/resolvable";

let uiRouterFactory = (routerConfig: UIRouterConfig, location: UIRouterLocation, injector: Injector) => {
services.$injector.get = injector.get.bind(injector);
Expand All @@ -74,6 +75,9 @@ let uiRouterFactory = (routerConfig: UIRouterConfig, location: UIRouterLocation,
router.stateRegistry.decorator('views', ng2ViewsBuilder);

router.stateRegistry.stateQueue.autoFlush(router.stateService);

let ng2InjectorResolvable = new Resolvable(NG2_INJECTOR_TOKEN, () => injector, null, { when: "EAGER" }, injector);
router.stateRegistry.root().resolvables.push(ng2InjectorResolvable);

setTimeout(() => {
routerConfig.configure(router);
Expand Down
2 changes: 1 addition & 1 deletion src/ng2/routerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function UIRouterModule(moduleMetaData: UIRouterModuleMetadata) {
let components = states.map(state => state.views || { $default: state })
.map(viewObj => Object.keys(viewObj).map(key => viewObj[key].component))
.reduce((acc, arr) => acc.concat(arr), [])
.filter(x => typeof x === 'function');
.filter(x => typeof x === 'function' && x !== UIView);

moduleMetaData.imports = <any[]> (moduleMetaData.imports || []).concat(_UIRouterModule).reduce(uniqR, []);
moduleMetaData.declarations = <any[]> (moduleMetaData.declarations || []).concat(components).reduce(uniqR, []);
Expand Down

0 comments on commit 37241e7

Please sign in to comment.