fix(ivy): align NgModuleRef implementation between Ivy and ViewEngine (#27482)

Solves FW-765 and FW-767

PR Close #27482
This commit is contained in:
Marc Laval 2018-12-05 17:43:59 +01:00 committed by Igor Minar
parent 159ab1c257
commit 8e9858fadb
6 changed files with 353 additions and 254 deletions

View File

@ -34,10 +34,15 @@ import {createElementRef} from './view_engine_compatibility';
import {RootViewRef, ViewRef} from './view_ref';
export class ComponentFactoryResolver extends viewEngine_ComponentFactoryResolver {
/**
* @param ngModule The NgModuleRef to which all resolved factories are bound.
*/
constructor(private ngModule?: viewEngine_NgModuleRef<any>) { super(); }
resolveComponentFactory<T>(component: Type<T>): viewEngine_ComponentFactory<T> {
ngDevMode && assertComponentType(component);
const componentDef = getComponentDef(component) !;
return new ComponentFactory(componentDef);
return new ComponentFactory(componentDef, this.ngModule);
}
}
@ -75,10 +80,13 @@ function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injec
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => {
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
// Return the value from the root element injector when
// - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value;
}
@ -103,7 +111,12 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
return toRefArray(this.componentDef.outputs);
}
constructor(private componentDef: ComponentDef<any>) {
/**
* @param componentDef The component definition.
* @param ngModule The NgModuleRef to which the factory is bound.
*/
constructor(
private componentDef: ComponentDef<any>, private ngModule?: viewEngine_NgModuleRef<any>) {
super();
this.componentType = componentDef.type;
this.selector = componentDef.selectors[0][0] as string;
@ -114,6 +127,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
injector: Injector, projectableNodes?: any[][]|undefined, rootSelectorOrNode?: any,
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<T> {
const isInternalRootView = rootSelectorOrNode === undefined;
ngModule = ngModule || this.ngModule;
const rootViewInjector =
ngModule ? createChainedInjector(injector, ngModule.injector) : injector;

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '../di/injector';
import {INJECTOR, Injector} from '../di/injector';
import {InjectFlags} from '../di/injector_compatibility';
import {StaticProvider} from '../di/provider';
import {createInjector} from '../di/r3_injector';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
@ -14,27 +15,29 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo
import {NgModuleDef} from '../metadata/ng_module';
import {Type} from '../type';
import {stringify} from '../util';
import {assertDefined} from './assert';
import {ComponentFactoryResolver} from './component_ref';
import {getNgModuleDef} from './definition';
export interface NgModuleType { ngModuleDef: NgModuleDef<any>; }
export const COMPONENT_FACTORY_RESOLVER: StaticProvider = {
const COMPONENT_FACTORY_RESOLVER: StaticProvider = {
provide: viewEngine_ComponentFactoryResolver,
useFactory: () => new ComponentFactoryResolver(),
deps: [],
useClass: ComponentFactoryResolver,
deps: [viewEngine_NgModuleRef],
};
export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements InternalNgModuleRef<T> {
// tslint:disable-next-line:require-internal-with-underscore
_bootstrapComponents: Type<any>[] = [];
injector: Injector;
componentFactoryResolver: viewEngine_ComponentFactoryResolver;
// tslint:disable-next-line:require-internal-with-underscore
_r3Injector: Injector;
injector: Injector = this;
instance: T;
destroyCbs: (() => void)[]|null = [];
constructor(ngModuleType: Type<T>, parentInjector: Injector|null) {
constructor(ngModuleType: Type<T>, public _parent: Injector|null) {
super();
const ngModuleDef = getNgModuleDef(ngModuleType);
ngDevMode && assertDefined(
@ -43,14 +46,26 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
this._bootstrapComponents = ngModuleDef !.bootstrap;
const additionalProviders: StaticProvider[] = [
COMPONENT_FACTORY_RESOLVER, {
{
provide: viewEngine_NgModuleRef,
useValue: this,
}
},
COMPONENT_FACTORY_RESOLVER
];
this.injector = createInjector(ngModuleType, parentInjector, additionalProviders);
this.instance = this.injector.get(ngModuleType);
this.componentFactoryResolver = new ComponentFactoryResolver();
this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders);
this.instance = this.get(ngModuleType);
}
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
injectFlags: InjectFlags = InjectFlags.Default): any {
if (token === Injector || token === viewEngine_NgModuleRef || token === INJECTOR) {
return this;
}
return this._r3Injector.get(token, notFoundValue, injectFlags);
}
get componentFactoryResolver(): viewEngine_ComponentFactoryResolver {
return this.get(viewEngine_ComponentFactoryResolver);
}
destroy(): void {

View File

@ -235,7 +235,7 @@ export function createContainerRef(
injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
ngModuleRef?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
const contextInjector = injector || this.parentInjector;
if (!ngModuleRef && contextInjector) {
if (!ngModuleRef && (componentFactory as any).ngModule == null && contextInjector) {
ngModuleRef = contextInjector.get(viewEngine_NgModuleRef, null);
}

View File

@ -187,5 +187,87 @@ describe('ComponentFactory', () => {
expect(mSanitizerFactorySpy).toHaveBeenCalled();
});
});
describe('(when the factory is bound to a `ngModuleRef`)', () => {
it('should retrieve `RendererFactory2` from the specified injector first', () => {
const injector = Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer2Spy}},
]);
(cf as any).ngModule = {
injector: Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer3Spy}},
])
};
cf.create(injector);
expect(createRenderer2Spy).toHaveBeenCalled();
expect(createRenderer3Spy).not.toHaveBeenCalled();
});
it('should retrieve `RendererFactory2` from the `ngModuleRef` if not provided by the injector',
() => {
const injector = Injector.create([]);
(cf as any).ngModule = {
injector: Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer2Spy}},
])
};
cf.create(injector);
expect(createRenderer2Spy).toHaveBeenCalled();
expect(createRenderer3Spy).not.toHaveBeenCalled();
});
it('should fall back to `domRendererFactory3` if `RendererFactory2` is not provided', () => {
const injector = Injector.create([]);
(cf as any).ngModule = {injector: Injector.create([])};
cf.create(injector);
expect(createRenderer2Spy).not.toHaveBeenCalled();
expect(createRenderer3Spy).toHaveBeenCalled();
});
it('should retrieve `Sanitizer` from the specified injector first', () => {
const iSanitizerFactorySpy =
jasmine.createSpy('Injector#sanitizerFactory').and.returnValue({});
const injector = Injector.create([
{provide: Sanitizer, useFactory: iSanitizerFactorySpy, deps: []},
]);
const mSanitizerFactorySpy =
jasmine.createSpy('NgModuleRef#sanitizerFactory').and.returnValue({});
(cf as any).ngModule = {
injector: Injector.create([
{provide: Sanitizer, useFactory: mSanitizerFactorySpy, deps: []},
])
};
cf.create(injector);
expect(iSanitizerFactorySpy).toHaveBeenCalled();
expect(mSanitizerFactorySpy).not.toHaveBeenCalled();
});
it('should retrieve `Sanitizer` from the `ngModuleRef` if not provided by the injector',
() => {
const injector = Injector.create([]);
const mSanitizerFactorySpy =
jasmine.createSpy('NgModuleRef#sanitizerFactory').and.returnValue({});
(cf as any).ngModule = {
injector: Injector.create([
{provide: Sanitizer, useFactory: mSanitizerFactorySpy, deps: []},
])
};
cf.create(injector);
expect(mSanitizerFactorySpy).toHaveBeenCalled();
});
});
});
});

View File

@ -3561,8 +3561,7 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
})));
fixmeIvy('FW-646: Directive providers don\'t support primitive types as DI tokens')
.it('should have 2 injector trees: module and element',
it('should have 2 injector trees: module and element',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@ -3689,9 +3688,7 @@ describe('Integration', () => {
})));
// https://github.com/angular/angular/issues/13870
fixmeIvy(
'FW-767: Lazy loaded modules are not used when resolving dependencies in one of their components')
.it('should create a single instance of guards for lazy-loaded modules',
it('should create a single instance of guards for lazy-loaded modules',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@ -3736,8 +3733,8 @@ describe('Integration', () => {
advance(fixture);
expect(fixture.nativeElement).toHaveText('lazy');
const lzc = fixture.debugElement.query(By.directive(LazyLoadedComponent))
.componentInstance;
const lzc =
fixture.debugElement.query(By.directive(LazyLoadedComponent)).componentInstance;
expect(lzc.injectedService).toBe(lzc.resolvedService);
})));
@ -3990,9 +3987,7 @@ describe('Integration', () => {
});
});
fixmeIvy(
'FW-767: Lazy loaded modules are not used when resolving dependencies in one of their components')
.it('should use the injector of the lazily-loaded configuration',
it('should use the injector of the lazily-loaded configuration',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {

View File

@ -8,7 +8,6 @@
import {Compiler, Component, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {fixmeIvy} from '@angular/private/testing';
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router';
import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
@ -60,10 +59,7 @@ describe('RouterPreloader', () => {
});
});
fixmeIvy(
'FW-765: NgModuleRef hierarchy is differently constructed when the router preloads modules')
.it('should work',
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router,
@ -71,15 +67,15 @@ describe('RouterPreloader', () => {
const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild(
[{path: 'LoadedModule2', component: LazyLoadedCmp}])]
imports:
[RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
})
class LoadedModule2 {
}
@NgModule({
imports: [RouterModule.forChild(
[{path: 'LoadedModule1', loadChildren: 'expected2'}])]
imports:
[RouterModule.forChild([{path: 'LoadedModule1', loadChildren: 'expected2'}])]
})
class LoadedModule1 {
}
@ -130,13 +126,10 @@ describe('RouterPreloader', () => {
});
});
fixmeIvy(
'FW-765: NgModuleRef hierarchy is differently constructed when the router preloads modules')
.it('should work',
fakeAsync(inject(
it('should work', fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef, Compiler],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router,
testModule: NgModuleRef<any>, compiler: Compiler) => {
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader,
router: Router, testModule: NgModuleRef<any>, compiler: Compiler) => {
@NgModule()
class LoadedModule2 {
}