Skip to content

Commit

Permalink
fix(ivy): align NgModuleRef implementation between Ivy and ViewEngine
Browse files Browse the repository at this point in the history
  • Loading branch information
marclaval committed Dec 5, 2018
1 parent 4b9948c commit 1b2ace1
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 191 deletions.
15 changes: 13 additions & 2 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -103,7 +108,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;
Expand All @@ -114,6 +124,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;
Expand Down
33 changes: 22 additions & 11 deletions packages/core/src/render3/ng_module_ref.ts
Expand Up @@ -7,6 +7,7 @@
*/

import {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';
Expand All @@ -20,21 +21,22 @@ 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(
Expand All @@ -43,14 +45,23 @@ 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 {
return this._r3Injector.get(token, notFoundValue, injectFlags);
}

get componentFactoryResolver(): viewEngine_ComponentFactoryResolver {
return this.get(viewEngine_ComponentFactoryResolver);
}

destroy(): void {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render3/view_engine_compatibility.ts
Expand Up @@ -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);
}

Expand Down
82 changes: 82 additions & 0 deletions packages/core/test/render3/component_ref_spec.ts
Expand Up @@ -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();
});
});
});
});
124 changes: 60 additions & 64 deletions packages/router/test/integration.spec.ts
Expand Up @@ -3652,57 +3652,55 @@ 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',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Injectable()
class Service {
}
it('should create a single instance of guards for lazy-loaded modules',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Injectable()
class Service {
}

@Injectable()
class Resolver implements Resolve<Service> {
constructor(public service: Service) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.service;
}
}
@Injectable()
class Resolver implements Resolve<Service> {
constructor(public service: Service) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.service;
}
}

@Component({selector: 'lazy', template: 'lazy'})
class LazyLoadedComponent {
resolvedService: Service;
constructor(public injectedService: Service, route: ActivatedRoute) {
this.resolvedService = route.snapshot.data['service'];
}
}
@Component({selector: 'lazy', template: 'lazy'})
class LazyLoadedComponent {
resolvedService: Service;
constructor(public injectedService: Service, route: ActivatedRoute) {
this.resolvedService = route.snapshot.data['service'];
}
}

@NgModule({
declarations: [LazyLoadedComponent],
providers: [Service, Resolver],
imports: [
RouterModule.forChild([{
path: 'loaded',
component: LazyLoadedComponent,
resolve: {'service': Resolver},
}]),
]
})
class LoadedModule {
}
@NgModule({
declarations: [LazyLoadedComponent],
providers: [Service, Resolver],
imports: [
RouterModule.forChild([{
path: 'loaded',
component: LazyLoadedComponent,
resolve: {'service': Resolver},
}]),
]
})
class LoadedModule {
}

loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded');
advance(fixture);
loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded');
advance(fixture);

expect(fixture.nativeElement).toHaveText('lazy');
const lzc = fixture.debugElement.query(By.directive(LazyLoadedComponent))
.componentInstance;
expect(lzc.injectedService).toBe(lzc.resolvedService);
})));
expect(fixture.nativeElement).toHaveText('lazy');
const lzc =
fixture.debugElement.query(By.directive(LazyLoadedComponent)).componentInstance;
expect(lzc.injectedService).toBe(lzc.resolvedService);
})));


it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded',
Expand Down Expand Up @@ -3953,28 +3951,26 @@ 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',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
loader.stubbedModules = {expected: LoadedModule};
it('should use the injector of the lazily-loaded configuration',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
loader.stubbedModules = {expected: LoadedModule};

const fixture = createRoot(router, RootCmp);
const fixture = createRoot(router, RootCmp);

router.resetConfig([{
path: 'eager-parent',
component: EagerParentComponent,
children: [{path: 'lazy', loadChildren: 'expected'}]
}]);
router.resetConfig([{
path: 'eager-parent',
component: EagerParentComponent,
children: [{path: 'lazy', loadChildren: 'expected'}]
}]);

router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child');
advance(fixture);
router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child');
advance(fixture);

expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child');
expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child');
})));
expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child');
expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child');
})));
});

it('works when given a callback',
Expand Down

0 comments on commit 1b2ace1

Please sign in to comment.