fix(ivy): detach ViewRef from ApplicationRef on destroy (#27276)

Currently we store the `_appRef` when a `ViewRef` is attached, however we don't use it for anything. These changes use it to detach the view from the `ApplicationRef` when it is destroyed. These changes also fix that the `ComponentRef` doesn't remove its `ViewRef` on destroy.

PR Close #27276
This commit is contained in:
Kristiyan Kostadinov 2018-11-26 21:56:29 +01:00 committed by Jason Aden
parent 0487fbe236
commit 4622d0b23a
2 changed files with 265 additions and 231 deletions

View File

@ -59,7 +59,9 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
} }
destroy(): void { destroy(): void {
if (this._viewContainerRef && viewAttached(this._view)) { if (this._appRef) {
this._appRef.detachView(this);
} else if (this._viewContainerRef && viewAttached(this._view)) {
this._viewContainerRef.detach(this._viewContainerRef.indexOf(this)); this._viewContainerRef.detach(this._viewContainerRef.indexOf(this));
this._viewContainerRef = null; this._viewContainerRef = null;
} }

View File

@ -25,7 +25,7 @@ class SomeComponent {
} }
{ {
fixmeIvy('unknown') && describe('bootstrap', () => { describe('bootstrap', () => {
let mockConsole: MockConsole; let mockConsole: MockConsole;
beforeEach(() => { mockConsole = new MockConsole(); }); beforeEach(() => { mockConsole = new MockConsole(); });
@ -74,66 +74,68 @@ class SomeComponent {
return MyModule; return MyModule;
} }
it('should bootstrap a component from a child module', fixmeIvy('unknown') &&
async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => { it('should bootstrap a component from a child module',
@Component({ async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
selector: 'bootstrap-app', @Component({
template: '', selector: 'bootstrap-app',
}) template: '',
class SomeComponent { })
} class SomeComponent {
}
@NgModule({ @NgModule({
providers: [{provide: 'hello', useValue: 'component'}], providers: [{provide: 'hello', useValue: 'component'}],
declarations: [SomeComponent], declarations: [SomeComponent],
entryComponents: [SomeComponent], entryComponents: [SomeComponent],
}) })
class SomeModule { class SomeModule {
} }
createRootEl(); createRootEl();
const modFactory = compiler.compileModuleSync(SomeModule); const modFactory = compiler.compileModuleSync(SomeModule);
const module = modFactory.create(TestBed); const module = modFactory.create(TestBed);
const cmpFactory = const cmpFactory =
module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !; module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !;
const component = app.bootstrap(cmpFactory); const component = app.bootstrap(cmpFactory);
// The component should see the child module providers // The component should see the child module providers
expect(component.injector.get('hello')).toEqual('component'); expect(component.injector.get('hello')).toEqual('component');
}))); })));
it('should bootstrap a component with a custom selector', fixmeIvy('unknown') &&
async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => { it('should bootstrap a component with a custom selector',
@Component({ async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
selector: 'bootstrap-app', @Component({
template: '', selector: 'bootstrap-app',
}) template: '',
class SomeComponent { })
} class SomeComponent {
}
@NgModule({ @NgModule({
providers: [{provide: 'hello', useValue: 'component'}], providers: [{provide: 'hello', useValue: 'component'}],
declarations: [SomeComponent], declarations: [SomeComponent],
entryComponents: [SomeComponent], entryComponents: [SomeComponent],
}) })
class SomeModule { class SomeModule {
} }
createRootEl('custom-selector'); createRootEl('custom-selector');
const modFactory = compiler.compileModuleSync(SomeModule); const modFactory = compiler.compileModuleSync(SomeModule);
const module = modFactory.create(TestBed); const module = modFactory.create(TestBed);
const cmpFactory = const cmpFactory =
module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !; module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !;
const component = app.bootstrap(cmpFactory, 'custom-selector'); const component = app.bootstrap(cmpFactory, 'custom-selector');
// The component should see the child module providers // The component should see the child module providers
expect(component.injector.get('hello')).toEqual('component'); expect(component.injector.get('hello')).toEqual('component');
}))); })));
describe('ApplicationRef', () => { describe('ApplicationRef', () => {
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
it('should throw when reentering tick', () => { fixmeIvy('unknown') && it('should throw when reentering tick', () => {
@Component({template: '{{reenter()}}'}) @Component({template: '{{reenter()}}'})
class ReenteringComponent { class ReenteringComponent {
reenterCount = 1; reenterCount = 1;
@ -174,28 +176,31 @@ class SomeComponent {
}); });
}); });
it('should be called when a component is bootstrapped', fixmeIvy('unknown') && it('should be called when a component is bootstrapped',
inject([ApplicationRef], (ref: ApplicationRef) => { inject([ApplicationRef], (ref: ApplicationRef) => {
createRootEl(); createRootEl();
const compRef = ref.bootstrap(SomeComponent); const compRef = ref.bootstrap(SomeComponent);
expect(capturedCompRefs).toEqual([compRef]); expect(capturedCompRefs).toEqual([compRef]);
})); }));
}); });
describe('bootstrap', () => { describe('bootstrap', () => {
it('should throw if an APP_INITIIALIZER is not yet resolved', fixmeIvy('unknown') &&
withModule( it('should throw if an APP_INITIIALIZER is not yet resolved',
{ withModule(
providers: [ {
{provide: APP_INITIALIZER, useValue: () => new Promise(() => {}), multi: true} providers: [{
] provide: APP_INITIALIZER,
}, useValue: () => new Promise(() => {}),
inject([ApplicationRef], (ref: ApplicationRef) => { multi: true
createRootEl(); }]
expect(() => ref.bootstrap(SomeComponent)) },
.toThrowError( inject([ApplicationRef], (ref: ApplicationRef) => {
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.'); createRootEl();
}))); expect(() => ref.bootstrap(SomeComponent))
.toThrowError(
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
})));
}); });
}); });
@ -206,99 +211,112 @@ class SomeComponent {
defaultPlatform = _platform; defaultPlatform = _platform;
})); }));
it('should wait for asynchronous app initializers', async(() => { fixmeIvy('unknown') &&
let resolve: (result: any) => void; it('should wait for asynchronous app initializers', async(() => {
const promise: Promise<any> = new Promise((res) => { resolve = res; }); let resolve: (result: any) => void;
let initializerDone = false; const promise: Promise<any> = new Promise((res) => { resolve = res; });
setTimeout(() => { let initializerDone = false;
resolve(true); setTimeout(() => {
initializerDone = true; resolve(true);
}, 1); initializerDone = true;
}, 1);
defaultPlatform defaultPlatform
.bootstrapModule( .bootstrapModule(createModule(
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}])) [{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]))
.then(_ => { expect(initializerDone).toBe(true); }); .then(_ => { expect(initializerDone).toBe(true); });
})); }));
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => { fixmeIvy('unknown') &&
defaultPlatform it('should rethrow sync errors even if the exceptionHandler is not rethrowing',
.bootstrapModule(createModule( async(() => {
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}])) defaultPlatform
.then(() => expect(false).toBe(true), (e) => { .bootstrapModule(createModule([
expect(e).toBe('Test'); {provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}
// Error rethrown will be seen by the exception handler since it's after ]))
// construction. .then(() => expect(false).toBe(true), (e) => {
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test'); expect(e).toBe('Test');
}); // Error rethrown will be seen by the exception handler since it's after
})); // construction.
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
});
}));
it('should rethrow promise errors even if the exceptionHandler is not rethrowing', fixmeIvy('unknown') &&
async(() => { it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
defaultPlatform async(() => {
.bootstrapModule(createModule([ defaultPlatform
{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true} .bootstrapModule(createModule([{
])) provide: APP_INITIALIZER,
.then(() => expect(false).toBe(true), (e) => { useValue: () => Promise.reject('Test'),
expect(e).toBe('Test'); multi: true
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test'); }]))
}); .then(() => expect(false).toBe(true), (e) => {
})); expect(e).toBe('Test');
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
});
}));
it('should throw useful error when ApplicationRef is not configured', async(() => { fixmeIvy('unknown') &&
@NgModule() it('should throw useful error when ApplicationRef is not configured', async(() => {
class EmptyModule { @NgModule()
} class EmptyModule {
}
return defaultPlatform.bootstrapModule(EmptyModule) return defaultPlatform.bootstrapModule(EmptyModule)
.then(() => fail('expecting error'), (error) => { .then(() => fail('expecting error'), (error) => {
expect(error.message) expect(error.message)
.toEqual('No ErrorHandler. Is platform module (BrowserModule) included?'); .toEqual('No ErrorHandler. Is platform module (BrowserModule) included?');
}); });
})); }));
it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module', fixmeIvy('unknown') &&
async(() => { it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module',
const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap'); async(() => {
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap})) const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
.then((moduleRef) => { defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap}))
const appRef = moduleRef.injector.get(ApplicationRef); .then((moduleRef) => {
expect(ngDoBootstrap).toHaveBeenCalledWith(appRef); const appRef = moduleRef.injector.get(ApplicationRef);
}); expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
})); });
}));
it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => { fixmeIvy('unknown') &&
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]})) it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => {
.then((moduleRef) => { defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); .then((moduleRef) => {
expect(appRef.componentTypes).toEqual([SomeComponent]); const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
}); expect(appRef.componentTypes).toEqual([SomeComponent]);
})); });
}));
it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified', fixmeIvy('unknown') &&
async(() => { it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified',
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false})) async(() => {
.then(() => expect(false).toBe(true), (e) => { defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false}))
const expectedErrMsg = .then(() => expect(false).toBe(true), (e) => {
`The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`; const expectedErrMsg =
expect(e.message).toEqual(expectedErrMsg); `The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`;
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg); expect(e.message).toEqual(expectedErrMsg);
}); expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg);
})); });
}));
it('should add bootstrapped module into platform modules list', async(() => { fixmeIvy('unknown') &&
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]})) it('should add bootstrapped module into platform modules list', async(() => {
.then(module => expect((<any>defaultPlatform)._modules).toContain(module)); defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
})); .then(module => expect((<any>defaultPlatform)._modules).toContain(module));
}));
it('should bootstrap with NoopNgZone', async(() => { fixmeIvy('unknown') &&
defaultPlatform it('should bootstrap with NoopNgZone', async(() => {
.bootstrapModule(createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'}) defaultPlatform
.then((module) => { .bootstrapModule(createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'})
const ngZone = module.injector.get(NgZone); .then((module) => {
expect(ngZone instanceof NoopNgZone).toBe(true); const ngZone = module.injector.get(NgZone);
}); expect(ngZone instanceof NoopNgZone).toBe(true);
})); });
}));
}); });
describe('bootstrapModuleFactory', () => { describe('bootstrapModuleFactory', () => {
@ -307,47 +325,58 @@ class SomeComponent {
createRootEl(); createRootEl();
defaultPlatform = _platform; defaultPlatform = _platform;
})); }));
it('should wait for asynchronous app initializers', async(() => { fixmeIvy('unknown') &&
let resolve: (result: any) => void; it('should wait for asynchronous app initializers', async(() => {
const promise: Promise<any> = new Promise((res) => { resolve = res; }); let resolve: (result: any) => void;
let initializerDone = false; const promise: Promise<any> = new Promise((res) => { resolve = res; });
setTimeout(() => { let initializerDone = false;
resolve(true); setTimeout(() => {
initializerDone = true; resolve(true);
}, 1); initializerDone = true;
}, 1);
const compilerFactory: CompilerFactory = const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null); defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync( const moduleFactory =
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}])); compilerFactory.createCompiler().compileModuleSync(createModule(
defaultPlatform.bootstrapModuleFactory(moduleFactory).then(_ => { [{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]));
expect(initializerDone).toBe(true); defaultPlatform.bootstrapModuleFactory(moduleFactory).then(_ => {
}); expect(initializerDone).toBe(true);
}));
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
// Error rethrown will be seen by the exception handler since it's after
// construction.
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
}));
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}]));
defaultPlatform.bootstrapModuleFactory(moduleFactory)
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
}); });
})); }));
fixmeIvy('unknown') &&
it('should rethrow sync errors even if the exceptionHandler is not rethrowing',
async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory =
compilerFactory.createCompiler().compileModuleSync(createModule([
{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}
]));
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
// Error rethrown will be seen by the exception handler since it's after
// construction.
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
}));
fixmeIvy('unknown') &&
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory =
compilerFactory.createCompiler().compileModuleSync(createModule([{
provide: APP_INITIALIZER,
useValue: () => Promise.reject('Test'),
multi: true
}]));
defaultPlatform.bootstrapModuleFactory(moduleFactory)
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
});
}));
}); });
describe('attachView / detachView', () => { describe('attachView / detachView', () => {
@ -427,28 +456,29 @@ class SomeComponent {
expect(appRef.viewCount).toBe(0); expect(appRef.viewCount).toBe(0);
}); });
it('should not allow to attach a view to both, a view container and the ApplicationRef', fixmeIvy('unknown') &&
() => { it('should not allow to attach a view to both, a view container and the ApplicationRef',
const comp = TestBed.createComponent(MyComp); () => {
let hostView = comp.componentRef.hostView; const comp = TestBed.createComponent(MyComp);
const containerComp = TestBed.createComponent(ContainerComp); let hostView = comp.componentRef.hostView;
containerComp.detectChanges(); const containerComp = TestBed.createComponent(ContainerComp);
const vc = containerComp.componentInstance.vc; containerComp.detectChanges();
const appRef: ApplicationRef = TestBed.get(ApplicationRef); const vc = containerComp.componentInstance.vc;
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
vc.insert(hostView); vc.insert(hostView);
expect(() => appRef.attachView(hostView)) expect(() => appRef.attachView(hostView))
.toThrowError('This view is already attached to a ViewContainer!'); .toThrowError('This view is already attached to a ViewContainer!');
hostView = vc.detach(0) !; hostView = vc.detach(0) !;
appRef.attachView(hostView); appRef.attachView(hostView);
expect(() => vc.insert(hostView)) expect(() => vc.insert(hostView))
.toThrowError('This view is already attached directly to the ApplicationRef!'); .toThrowError('This view is already attached directly to the ApplicationRef!');
}); });
}); });
}); });
fixmeIvy('unknown') && describe('AppRef', () => { describe('AppRef', () => {
@Component({selector: 'sync-comp', template: `<span>{{text}}</span>`}) @Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
class SyncComp { class SyncComp {
text: string = '1'; text: string = '1';
@ -535,20 +565,22 @@ class SomeComponent {
}); });
} }
it('isStable should fire on synchronous component loading', fixmeIvy('unknown') && it('isStable should fire on synchronous component loading',
async(() => { expectStableTexts(SyncComp, ['1']); })); async(() => { expectStableTexts(SyncComp, ['1']); }));
it('isStable should fire after a microtask on init is completed', fixmeIvy('unknown') && it('isStable should fire after a microtask on init is completed',
async(() => { expectStableTexts(MicroTaskComp, ['11']); })); async(() => { expectStableTexts(MicroTaskComp, ['11']); }));
it('isStable should fire after a macrotask on init is completed', fixmeIvy('unknown') && it('isStable should fire after a macrotask on init is completed',
async(() => { expectStableTexts(MacroTaskComp, ['11']); })); async(() => { expectStableTexts(MacroTaskComp, ['11']); }));
it('isStable should fire only after chain of micro and macrotasks on init are completed', fixmeIvy('unknown') &&
async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); })); it('isStable should fire only after chain of micro and macrotasks on init are completed',
async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); }));
it('isStable should fire only after chain of macro and microtasks on init are completed', fixmeIvy('unknown') &&
async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); })); it('isStable should fire only after chain of macro and microtasks on init are completed',
async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); }));
describe('unstable', () => { describe('unstable', () => {
let unstableCalled = false; let unstableCalled = false;
@ -569,19 +601,19 @@ class SomeComponent {
}); });
} }
it('should be fired after app becomes unstable', async(() => { fixmeIvy('unknown') && it('should be fired after app becomes unstable', async(() => {
const fixture = TestBed.createComponent(ClickComp); const fixture = TestBed.createComponent(ClickComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef); const appRef: ApplicationRef = TestBed.get(ApplicationRef);
const zone: NgZone = TestBed.get(NgZone); const zone: NgZone = TestBed.get(NgZone);
appRef.attachView(fixture.componentRef.hostView); appRef.attachView(fixture.componentRef.hostView);
zone.run(() => appRef.tick()); zone.run(() => appRef.tick());
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
expectUnstable(appRef); expectUnstable(appRef);
const element = fixture.debugElement.children[0]; const element = fixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click'); dispatchEvent(element.nativeElement, 'click');
}); });
})); }));
}); });
}); });
} }