diff --git a/src/ng2/directives/uiView.ts b/src/ng2/directives/uiView.ts index a853c494e..3b95f3d02 100755 --- a/src/ng2/directives/uiView.ts +++ b/src/ng2/directives/uiView.ts @@ -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; @@ -132,14 +129,13 @@ export class UIView { @Input('ui-view') set _name(val: string) { this.name = val; } componentRef: ComponentRef; deregister: Function; - uiViewData: any = {}; + uiViewData: ActiveUIView = {}; static PARENT_INJECT = "UIView.PARENT_INJECT"; constructor( public router: UIRouter, @Inject(UIView.PARENT_INJECT) public parent: ParentUIViewInject, - public compFactoryResolver: ComponentFactoryResolver, public viewContainerRef: ViewContainerRef ) { } @@ -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 = 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 = viewDecl.component; - - let createComponent = (factory: ComponentFactory) => { - 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, 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(); + } +} diff --git a/src/ng2/interface.ts b/src/ng2/interface.ts index 9d0e9e715..3192d7ad7 100644 --- a/src/ng2/interface.ts +++ b/src/ng2/interface.ts @@ -341,3 +341,5 @@ export interface Ng2Component { */ uiCanExit(): HookResult; } + +export const NG2_INJECTOR_TOKEN = {}; diff --git a/src/ng2/mergeInjector.ts b/src/ng2/mergeInjector.ts new file mode 100644 index 000000000..ba90a61ad --- /dev/null +++ b/src/ng2/mergeInjector.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/ng2/providers.ts b/src/ng2/providers.ts index efefb3fd2..20c7b2bee 100644 --- a/src/ng2/providers.ts +++ b/src/ng2/providers.ts @@ -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); @@ -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); diff --git a/src/ng2/routerModule.ts b/src/ng2/routerModule.ts index 749c5da93..d1d8830bc 100644 --- a/src/ng2/routerModule.ts +++ b/src/ng2/routerModule.ts @@ -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 = (moduleMetaData.imports || []).concat(_UIRouterModule).reduce(uniqR, []); moduleMetaData.declarations = (moduleMetaData.declarations || []).concat(components).reduce(uniqR, []);