Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(platform-server): wait for async app initializers to complete before removing server side styles #16712

Merged
merged 1 commit into from May 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 31 additions & 6 deletions packages/core/src/application_init.ts
Expand Up @@ -24,23 +24,48 @@ export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Applicatio
*/
@Injectable()
export class ApplicationInitStatus {
private resolve: Function;
private reject: Function;
private initialized = false;
private _donePromise: Promise<any>;
private _done = false;

constructor(@Inject(APP_INITIALIZER) @Optional() appInits: (() => any)[]) {
constructor(@Inject(APP_INITIALIZER) @Optional() private appInits: (() => any)[]) {
this._donePromise = new Promise((res, rej) => {
this.resolve = res;
this.reject = rej;
});
}

/** @internal */
runInitializers() {
if (this.initialized) {
return;
}

const asyncInitPromises: Promise<any>[] = [];
if (appInits) {
for (let i = 0; i < appInits.length; i++) {
const initResult = appInits[i]();

const complete =
() => {
this._done = true;
this.resolve();
}

if (this.appInits) {
for (let i = 0; i < this.appInits.length; i++) {
const initResult = this.appInits[i]();
if (isPromise(initResult)) {
asyncInitPromises.push(initResult);
}
}
}
this._donePromise = Promise.all(asyncInitPromises).then(() => { this._done = true; });

Promise.all(asyncInitPromises).then(() => { complete(); }).catch(e => { this.reject(e); });

if (asyncInitPromises.length === 0) {
this._done = true;
complete();
}
this.initialized = true;
}

get done(): boolean { return this._done; }
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/application_ref.ts
Expand Up @@ -302,6 +302,7 @@ export class PlatformRef_ extends PlatformRef {
ngZone !.onError.subscribe({next: (error: any) => { exceptionHandler.handleError(error); }});
return _callAndReportToErrorHandler(exceptionHandler, () => {
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
this._moduleDoBootstrap(moduleRef);
return moduleRef;
Expand Down
26 changes: 23 additions & 3 deletions packages/core/test/application_init_spec.ts
Expand Up @@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {APP_INITIALIZER, ApplicationInitStatus} from '../src/application_init';
import {TestBed, async, inject} from '../testing';

Expand All @@ -14,27 +15,46 @@ export function main() {

it('should return true for `done`',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
status.runInitializers();
expect(status.done).toBe(true);
})));

it('should return a promise that resolves immediately for `donePromise`',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
status.runInitializers();
status.donePromise.then(() => { expect(status.done).toBe(true); });
})));
});

describe('with async initializers', () => {
let resolve: (result: any) => void;
let promise: Promise<any>;
let completerResolver = false;
beforeEach(() => {
let initializerFactory = (injector: Injector) => {
return () => {
const initStatus = injector.get(ApplicationInitStatus);
initStatus.donePromise.then(() => { expect(completerResolver).toBe(true); });
}
};
promise = new Promise((res) => { resolve = res; });
TestBed.configureTestingModule(
{providers: [{provide: APP_INITIALIZER, multi: true, useValue: () => promise}]});
TestBed.configureTestingModule({
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => promise},
{
provide: APP_INITIALIZER,
multi: true,
useFactory: initializerFactory,
deps: [Injector]
},
]
});
});

it('should update the status once all async initializers are done',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
let completerResolver = false;
status.runInitializers();

setTimeout(() => {
completerResolver = true;
resolve(null);
Expand Down
16 changes: 6 additions & 10 deletions packages/core/test/application_ref_spec.ts
Expand Up @@ -225,11 +225,9 @@ export function main() {
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
// Note: if the modules throws an error during construction,
// we don't have an injector and therefore no way of
// getting the exception handler. So
// the error is only rethrown but not logged via the exception handler.
expect(mockConsole.res).toEqual([]);
// Error rethrown will be seen by the exception handler since it's after
// construction.
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
});
}));

Expand Down Expand Up @@ -322,11 +320,9 @@ export function main() {
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
// Note: if the modules throws an error during construction,
// we don't have an injector and therefore no way of
// getting the exception handler. So
// the error is only rethrown but not logged via the exception handler.
expect(mockConsole.res).toEqual([]);
// 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',
Expand Down
5 changes: 4 additions & 1 deletion packages/core/testing/src/test_bed.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵERROR_COMPONENT_TYPE, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core';
import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵERROR_COMPONENT_TYPE, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core';

import {AsyncTestCompleter} from './async_test_completer';
import {ComponentFixture} from './component_fixture';
Expand Down Expand Up @@ -311,6 +311,9 @@ export class TestBed implements Injector {
const ngZoneInjector = ReflectiveInjector.resolveAndCreate(
[{provide: NgZone, useValue: ngZone}], this.platform.injector);
this._moduleRef = this._moduleFactory.create(ngZoneInjector);
// ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any
// before accessing it.
(this._moduleRef.injector.get(ApplicationInitStatus) as any).runInitializers();
this._instantiated = true;
}

Expand Down
25 changes: 14 additions & 11 deletions packages/platform-browser/src/browser/server-transition.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {APP_INITIALIZER, Inject, InjectionToken, Provider} from '@angular/core';
import {APP_INITIALIZER, ApplicationInitStatus, Inject, InjectionToken, Injector, Provider} from '@angular/core';

import {getDOM} from '../dom/dom_adapter';
import {DOCUMENT} from '../dom/dom_tokens';
Expand All @@ -17,22 +17,25 @@ import {DOCUMENT} from '../dom/dom_tokens';
*/
export const TRANSITION_ID = new InjectionToken('TRANSITION_ID');

export function bootstrapListenerFactory(transitionId: string, document: any) {
const factory = () => {
const dom = getDOM();
const styles: any[] =
Array.prototype.slice.apply(dom.querySelectorAll(document, `style[ng-transition]`));
styles.filter(el => dom.getAttribute(el, 'ng-transition') === transitionId)
.forEach(el => dom.remove(el));
export function appInitializerFactory(transitionId: string, document: any, injector: Injector) {
return () => {
// Wait for all application initializers to be completed before removing the styles set by
// the server.
injector.get(ApplicationInitStatus).donePromise.then(() => {
const dom = getDOM();
const styles: any[] =
Array.prototype.slice.apply(dom.querySelectorAll(document, `style[ng-transition]`));
styles.filter(el => dom.getAttribute(el, 'ng-transition') === transitionId)
.forEach(el => dom.remove(el));
});
};
return factory;
}

export const SERVER_TRANSITION_PROVIDERS: Provider[] = [
{
provide: APP_INITIALIZER,
useFactory: bootstrapListenerFactory,
deps: [TRANSITION_ID, DOCUMENT],
useFactory: appInitializerFactory,
deps: [TRANSITION_ID, DOCUMENT, Injector],
multi: true
},
];