Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions modules/@angular/common/src/location/platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {OpaqueToken} from '@angular/core';
/**
* This class should not be used directly by an application developer. Instead, use
* {@link Location}.
Expand Down Expand Up @@ -50,6 +51,12 @@ export abstract class PlatformLocation {
abstract back(): void;
}

/**
* @whatItDoes indicates when a location is initialized
* @experimental
*/
export const LOCATION_INITIALIZED = new OpaqueToken('Location Initialized');

/**
* A serializable version of the event from onPopState or onHashChange
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PlatformLocation} from '@angular/common';
import {APP_INITIALIZER, NgZone} from '@angular/core';
import {LOCATION_INITIALIZED, PlatformLocation} from '@angular/common';
import {APP_INITIALIZER, NgZone, OpaqueToken} from '@angular/core';

import {WebWorkerPlatformLocation} from './platform_location';



/**
* Those providers should be added when the router is used in a worker context in addition to the
* {@link ROUTER_PROVIDERS} and after them.
Expand All @@ -23,10 +24,15 @@ export const WORKER_APP_LOCATION_PROVIDERS = [
useFactory: appInitFnFactory,
multi: true,
deps: [PlatformLocation, NgZone]
}
},
{provide: LOCATION_INITIALIZED, useFactory: locationInitialized, deps: [PlatformLocation]}
];

function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
export function locationInitialized(platformLocation: WebWorkerPlatformLocation) {
return platformLocation.initialized;
}

export function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
Promise<boolean> {
return () => zone.runGuarded(() => platformLocation.init());
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
private _hashChangeListeners: Array<Function> = [];
private _location: LocationType = null;
private _channelSource: EventEmitter<Object>;
public initialized: Promise<any>;
private initializedResolve: () => void;

constructor(
brokerFactory: ClientMessageBrokerFactory, bus: MessageBus, private _serializer: Serializer) {
Expand Down Expand Up @@ -52,6 +54,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
}
}
});
this.initialized = new Promise(res => this.initializedResolve = res);
}

/** @internal **/
Expand All @@ -63,6 +66,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
(val: LocationType):
boolean => {
this._location = val;
this.initializedResolve();
return true;
},
(err): boolean => { throw new Error(err); });
Expand Down
55 changes: 45 additions & 10 deletions modules/@angular/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ type NavigationParams = {
source: NavigationSource,
};

/**
* @internal
*/
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;

/**
* @internal
*/
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
return of (null);
}


/**
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
Expand Down Expand Up @@ -320,11 +332,23 @@ export class Router {
*/
errorHandler: ErrorHandler = defaultErrorHandler;



/**
* Indicates if at least one navigation happened.
*/
navigated: boolean = false;

/**
* Used by RouterModule. This allows us to
* pause the navigation either before preactivation or after it.
* @internal
*/
hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = {
beforePreactivation: defaultRouterHook,
afterPreactivation: defaultRouterHook
};

/**
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
*/
Expand Down Expand Up @@ -681,26 +705,33 @@ export class Router {
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
}

const beforePreactivationDone$ = mergeMap.call(
urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
return map.call(this.hooks.beforePreactivation(p.snapshot), () => p);
});

// run preactivation: guards and data resolvers
let preActivation: PreActivation;
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
preActivation =
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
});
const preactivationTraverse$ = map.call(
beforePreactivationDone$,
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
preActivation =
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
});

const preactivationCheckGuards =
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
const preactivationCheckGuards$ = mergeMap.call(
preactivationTraverse$,
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
if (this.navigationId !== id) return of (false);

return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});

const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => {
if (this.navigationId !== id) return of (false);

if (p.shouldActivate) {
Expand All @@ -710,11 +741,15 @@ export class Router {
}
});

const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => {
return map.call(this.hooks.afterPreactivation(p.snapshot), () => p);
});


// create router state
// this operation has side effects => route state is being affected
const routerState$ =
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state =
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);
Expand Down
94 changes: 77 additions & 17 deletions modules/@angular/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import {of } from 'rxjs/observable/of';

import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
Expand All @@ -19,7 +21,7 @@ import {ErrorHandler, Router} from './router';
import {ROUTES} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
import {ActivatedRoute} from './router_state';
import {ActivatedRoute, RouterStateSnapshot} from './router_state';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
import {flatten} from './utils/collection';
Expand Down Expand Up @@ -110,7 +112,7 @@ export function routerNgProbeToken() {
* In addition, we often want to split applications into multiple bundles and load them on demand.
* Doing this transparently is not trivial.
*
* The Angular 2 router solves these problems. Using the router, you can declaratively specify
* The Angular router solves these problems. Using the router, you can declaratively specify
* application states, manage state transitions while taking care of the URL, and load bundles on
* demand.
*
Expand Down Expand Up @@ -278,22 +280,77 @@ export function rootRoute(router: Router): ActivatedRoute {
return router.routerState.root;
}

export function initialRouterNavigation(
router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) {
return (bootstrappedComponentRef: ComponentRef<any>) => {
/**
* To initialize the router properly we need to do in two steps:
*
* We need to start the navigation in a APP_INITIALIZER to block the bootstrap if
* a resolver or a guards executes asynchronously. Second, we need to actually run
* activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation
* hook provided by the router to do that.
*
* The router navigation starts, reaches the point when preactivation is done, and then
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
*/
@Injectable()
export class RouterInitializer {
private initNavigation: boolean;
private resultOfPreactivationDone = new Subject<void>();

constructor(private injector: Injector) {}

appInitializer(): Promise<any> {
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
return p.then(() => {
let resolve: Function = null;
const res = new Promise(r => resolve = r);
const router = this.injector.get(Router);
const opts = this.injector.get(ROUTER_CONFIGURATION);

if (opts.initialNavigation === false) {
router.setUpLocationChangeListener();
} else {
router.hooks.afterPreactivation = () => {
// only the initial navigation should be delayed
if (!this.initNavigation) {
this.initNavigation = true;
resolve(true);
return this.resultOfPreactivationDone;

// subsequent navigations should not be delayed
} else {
return of (null);
}
};
router.initialNavigation();
}

return res;
});
}

bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
const ref = this.injector.get(ApplicationRef);
if (bootstrappedComponentRef !== ref.components[0]) {
return;
}

router.resetRootComponentType(ref.componentTypes[0]);
const preloader = this.injector.get(RouterPreloader);
preloader.setUpPreloading();
if (opts.initialNavigation === false) {
router.setUpLocationChangeListener();
} else {
router.initialNavigation();
}
};

const router = this.injector.get(Router);
router.resetRootComponentType(ref.componentTypes[0]);

this.resultOfPreactivationDone.next(null);
this.resultOfPreactivationDone.complete();
}
}

export function getAppInitializer(r: RouterInitializer) {
return r.appInitializer.bind(r);
}

export function getBootstrapListener(r: RouterInitializer) {
return r.bootstrapListener.bind(r);
}

/**
Expand All @@ -305,11 +362,14 @@ export const ROUTER_INITIALIZER = new OpaqueToken('Router Initializer');

export function provideRouterInitializer() {
return [
RouterInitializer,
{
provide: ROUTER_INITIALIZER,
useFactory: initialRouterNavigation,
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
provide: APP_INITIALIZER,
multi: true,
useFactory: getAppInitializer,
deps: [RouterInitializer]
},
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
];
}
Loading