fix(platform-server): wait for async app initializers to complete before removing server side styles (#16712)
This fixes a flicker when transitioning from server rendered page to client rendered page in lazy loaded routes by waiting for the lazy loaded route to finish loading, assuming initialNavigation on the route is set to 'enabled'. Fixes #15716
This commit is contained in:
parent
9a7f5d580f
commit
c805082648
|
@ -24,23 +24,48 @@ export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Applicatio
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApplicationInitStatus {
|
export class ApplicationInitStatus {
|
||||||
|
private resolve: Function;
|
||||||
|
private reject: Function;
|
||||||
|
private initialized = false;
|
||||||
private _donePromise: Promise<any>;
|
private _donePromise: Promise<any>;
|
||||||
private _done = false;
|
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>[] = [];
|
const asyncInitPromises: Promise<any>[] = [];
|
||||||
if (appInits) {
|
|
||||||
for (let i = 0; i < appInits.length; i++) {
|
const complete =
|
||||||
const initResult = appInits[i]();
|
() => {
|
||||||
|
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)) {
|
if (isPromise(initResult)) {
|
||||||
asyncInitPromises.push(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) {
|
if (asyncInitPromises.length === 0) {
|
||||||
this._done = true;
|
complete();
|
||||||
}
|
}
|
||||||
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get done(): boolean { return this._done; }
|
get done(): boolean { return this._done; }
|
||||||
|
|
|
@ -302,6 +302,7 @@ export class PlatformRef_ extends PlatformRef {
|
||||||
ngZone !.onError.subscribe({next: (error: any) => { exceptionHandler.handleError(error); }});
|
ngZone !.onError.subscribe({next: (error: any) => { exceptionHandler.handleError(error); }});
|
||||||
return _callAndReportToErrorHandler(exceptionHandler, () => {
|
return _callAndReportToErrorHandler(exceptionHandler, () => {
|
||||||
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
|
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
|
||||||
|
initStatus.runInitializers();
|
||||||
return initStatus.donePromise.then(() => {
|
return initStatus.donePromise.then(() => {
|
||||||
this._moduleDoBootstrap(moduleRef);
|
this._moduleDoBootstrap(moduleRef);
|
||||||
return moduleRef;
|
return moduleRef;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {Injector} from '@angular/core';
|
||||||
import {APP_INITIALIZER, ApplicationInitStatus} from '../src/application_init';
|
import {APP_INITIALIZER, ApplicationInitStatus} from '../src/application_init';
|
||||||
import {TestBed, async, inject} from '../testing';
|
import {TestBed, async, inject} from '../testing';
|
||||||
|
|
||||||
|
@ -14,11 +15,13 @@ export function main() {
|
||||||
|
|
||||||
it('should return true for `done`',
|
it('should return true for `done`',
|
||||||
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
||||||
|
status.runInitializers();
|
||||||
expect(status.done).toBe(true);
|
expect(status.done).toBe(true);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
it('should return a promise that resolves immediately for `donePromise`',
|
it('should return a promise that resolves immediately for `donePromise`',
|
||||||
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
||||||
|
status.runInitializers();
|
||||||
status.donePromise.then(() => { expect(status.done).toBe(true); });
|
status.donePromise.then(() => { expect(status.done).toBe(true); });
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
|
@ -26,15 +29,32 @@ export function main() {
|
||||||
describe('with async initializers', () => {
|
describe('with async initializers', () => {
|
||||||
let resolve: (result: any) => void;
|
let resolve: (result: any) => void;
|
||||||
let promise: Promise<any>;
|
let promise: Promise<any>;
|
||||||
|
let completerResolver = false;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
let initializerFactory = (injector: Injector) => {
|
||||||
|
return () => {
|
||||||
|
const initStatus = injector.get(ApplicationInitStatus);
|
||||||
|
initStatus.donePromise.then(() => { expect(completerResolver).toBe(true); });
|
||||||
|
}
|
||||||
|
};
|
||||||
promise = new Promise((res) => { resolve = res; });
|
promise = new Promise((res) => { resolve = res; });
|
||||||
TestBed.configureTestingModule(
|
TestBed.configureTestingModule({
|
||||||
{providers: [{provide: APP_INITIALIZER, multi: true, useValue: () => promise}]});
|
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',
|
it('should update the status once all async initializers are done',
|
||||||
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
|
||||||
let completerResolver = false;
|
status.runInitializers();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
completerResolver = true;
|
completerResolver = true;
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
|
|
@ -225,11 +225,9 @@ export function main() {
|
||||||
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
|
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
|
||||||
.then(() => expect(false).toBe(true), (e) => {
|
.then(() => expect(false).toBe(true), (e) => {
|
||||||
expect(e).toBe('Test');
|
expect(e).toBe('Test');
|
||||||
// Note: if the modules throws an error during construction,
|
// Error rethrown will be seen by the exception handler since it's after
|
||||||
// we don't have an injector and therefore no way of
|
// construction.
|
||||||
// getting the exception handler. So
|
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
|
||||||
// the error is only rethrown but not logged via the exception handler.
|
|
||||||
expect(mockConsole.res).toEqual([]);
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -322,11 +320,9 @@ export function main() {
|
||||||
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
|
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
|
||||||
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
|
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
|
||||||
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
|
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
|
||||||
// Note: if the modules throws an error during construction,
|
// Error rethrown will be seen by the exception handler since it's after
|
||||||
// we don't have an injector and therefore no way of
|
// construction.
|
||||||
// getting the exception handler. So
|
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
|
||||||
// the error is only rethrown but not logged via the exception handler.
|
|
||||||
expect(mockConsole.res).toEqual([]);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
|
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {AsyncTestCompleter} from './async_test_completer';
|
||||||
import {ComponentFixture} from './component_fixture';
|
import {ComponentFixture} from './component_fixture';
|
||||||
|
@ -311,6 +311,9 @@ export class TestBed implements Injector {
|
||||||
const ngZoneInjector = ReflectiveInjector.resolveAndCreate(
|
const ngZoneInjector = ReflectiveInjector.resolveAndCreate(
|
||||||
[{provide: NgZone, useValue: ngZone}], this.platform.injector);
|
[{provide: NgZone, useValue: ngZone}], this.platform.injector);
|
||||||
this._moduleRef = this._moduleFactory.create(ngZoneInjector);
|
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;
|
this._instantiated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {getDOM} from '../dom/dom_adapter';
|
||||||
import {DOCUMENT} from '../dom/dom_tokens';
|
import {DOCUMENT} from '../dom/dom_tokens';
|
||||||
|
@ -17,22 +17,25 @@ import {DOCUMENT} from '../dom/dom_tokens';
|
||||||
*/
|
*/
|
||||||
export const TRANSITION_ID = new InjectionToken('TRANSITION_ID');
|
export const TRANSITION_ID = new InjectionToken('TRANSITION_ID');
|
||||||
|
|
||||||
export function bootstrapListenerFactory(transitionId: string, document: any) {
|
export function appInitializerFactory(transitionId: string, document: any, injector: Injector) {
|
||||||
const factory = () => {
|
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 dom = getDOM();
|
||||||
const styles: any[] =
|
const styles: any[] =
|
||||||
Array.prototype.slice.apply(dom.querySelectorAll(document, `style[ng-transition]`));
|
Array.prototype.slice.apply(dom.querySelectorAll(document, `style[ng-transition]`));
|
||||||
styles.filter(el => dom.getAttribute(el, 'ng-transition') === transitionId)
|
styles.filter(el => dom.getAttribute(el, 'ng-transition') === transitionId)
|
||||||
.forEach(el => dom.remove(el));
|
.forEach(el => dom.remove(el));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return factory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SERVER_TRANSITION_PROVIDERS: Provider[] = [
|
export const SERVER_TRANSITION_PROVIDERS: Provider[] = [
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: bootstrapListenerFactory,
|
useFactory: appInitializerFactory,
|
||||||
deps: [TRANSITION_ID, DOCUMENT],
|
deps: [TRANSITION_ID, DOCUMENT, Injector],
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue