Skip to content

Commit

Permalink
feat(router): add support for ng1/ng2 migration (angular#12160)
Browse files Browse the repository at this point in the history
  • Loading branch information
vsavkin authored and btrigueiro committed Oct 21, 2016
1 parent a68a8ad commit 315042a
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 55 deletions.
1 change: 0 additions & 1 deletion modules/@angular/router/src/create_router_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
const value = prevState.value;
value._futureSnapshot = curr.value;

const children = createOrReuseChildren(curr, prevState);
return new TreeNode<ActivatedRoute>(value, children);

Expand Down
3 changes: 2 additions & 1 deletion modules/@angular/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {RouterOutletMap} from './router_outlet_map';
export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
export {PRIMARY_OUTLET, Params} from './shared';
export {DefaultUrlSerializer, UrlSegment, UrlSerializer, UrlTree} from './url_tree';
export {UrlHandlingStrategy} from './url_handling_strategy';
export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';

export * from './private_export'
120 changes: 85 additions & 35 deletions modules/@angular/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
import {NavigationCancelingError, PRIMARY_OUTLET, Params} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection';
import {TreeNode} from './utils/tree';
Expand Down Expand Up @@ -285,6 +286,9 @@ function defaultErrorHandler(error: any): any {
*/
export class Router {
private currentUrlTree: UrlTree;
private rawUrlTree: UrlTree;
private lastNavigation: UrlTree;

private currentRouterState: RouterState;
private locationSubscription: Subscription;
private routerEvents: Subject<Event>;
Expand All @@ -303,6 +307,11 @@ export class Router {
*/
navigated: boolean = false;

/**
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
*/
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();

/**
* Creates the router service.
*/
Expand All @@ -314,6 +323,7 @@ export class Router {
this.resetConfig(config);
this.routerEvents = new Subject<Event>();
this.currentUrlTree = createEmptyUrlTree();
this.rawUrlTree = this.currentUrlTree;
this.configLoader = new RouterConfigLoader(loader, compiler);
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
}
Expand Down Expand Up @@ -344,12 +354,20 @@ export class Router {
// Zone.current.wrap is needed because of the issue with RxJS scheduler,
// which does not work properly with zone.js in IE and Safari
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
const tree = this.urlSerializer.parse(change['url']);
// we fire multiple events for a single URL change
// we should navigate only once
return this.currentUrlTree.toString() !== tree.toString() ?
this.scheduleNavigation(tree, {skipLocationChange: change['pop'], replaceUrl: true}) :
null;
const rawUrlTree = this.urlSerializer.parse(change['url']);
const tree = this.urlHandlingStrategy.extract(rawUrlTree);


setTimeout(() => {
// we fire multiple events for a single URL change
// we should navigate only once
if (!this.lastNavigation || this.lastNavigation.toString() !== tree.toString()) {
this.scheduleNavigation(
rawUrlTree, tree, {skipLocationChange: change['pop'], replaceUrl: true});
} else {
this.rawUrlTree = rawUrlTree;
}
}, 0);
}));
}

Expand Down Expand Up @@ -470,10 +488,10 @@ export class Router {
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
if (url instanceof UrlTree) {
return this.scheduleNavigation(url, extras);
return this.scheduleNavigation(this.rawUrlTree, url, extras);
} else {
const urlTree = this.urlSerializer.parse(url);
return this.scheduleNavigation(urlTree, extras);
return this.scheduleNavigation(this.rawUrlTree, urlTree, extras);
}
}

Expand All @@ -500,7 +518,7 @@ export class Router {
*/
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
return this.scheduleNavigation(this.createUrlTree(commands, extras), extras);
return this.scheduleNavigation(this.rawUrlTree, this.createUrlTree(commands, extras), extras);
}

/**
Expand All @@ -525,16 +543,34 @@ export class Router {
}
}

private scheduleNavigation(url: UrlTree, extras: NavigationExtras): Promise<boolean> {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
return Promise.resolve().then(
(_) => this.runNavigate(url, extras.skipLocationChange, extras.replaceUrl, id));
private scheduleNavigation(rawUrl: UrlTree, url: UrlTree, extras: NavigationExtras):
Promise<boolean> {
if (this.urlHandlingStrategy.shouldProcessUrl(url)) {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));

return Promise.resolve().then(
(_) => this.runNavigate(
rawUrl, url, extras.skipLocationChange, extras.replaceUrl, id, null));

// we cannot process the current URL, but we could process the previous one =>
// we need to do some cleanup
} else if (this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));

return Promise.resolve().then(
(_) => this.runNavigate(
rawUrl, url, false, false, id, createEmptyState(url, this.rootComponentType)));
} else {
this.rawUrlTree = rawUrl;
return Promise.resolve(null);
}
}

private runNavigate(
url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number): Promise<boolean> {
rawUrl: UrlTree, url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number, precreatedState: RouterState): Promise<boolean> {
if (id !== this.navigationId) {
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
this.routerEvents.next(new NavigationCancel(
Expand All @@ -553,23 +589,33 @@ export class Router {
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;

const redirectsApplied$ = applyRedirects(this.injector, this.configLoader, url, this.config);

const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
appliedUrl = u;
return recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
});

const emitRecognzied$ = map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl), newRouterStateSnapshot));
return newRouterStateSnapshot;
});

const routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
});
let routerState$: any;

if (!precreatedState) {
const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, url, this.config);

const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
appliedUrl = u;
return recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
});

const emitRecognzied$ =
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
newRouterStateSnapshot));
return newRouterStateSnapshot;
});

routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
});
} else {
appliedUrl = url;
routerState$ = of (precreatedState);
}

const preactivation$ = map.call(routerState$, (newState: RouterState) => {
state = newState;
Expand All @@ -595,11 +641,14 @@ export class Router {
return;
}

this.lastNavigation = appliedUrl;
this.currentUrlTree = appliedUrl;
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);

this.currentRouterState = state;

if (!shouldPreventPushState) {
let path = this.urlSerializer.serialize(appliedUrl);
let path = this.urlSerializer.serialize(this.rawUrlTree);
if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) {
this.location.replaceState(path);
} else {
Expand Down Expand Up @@ -641,7 +690,8 @@ export class Router {
if (id === this.navigationId) {
this.currentRouterState = storedState;
this.currentUrlTree = storedUrl;
this.location.replaceState(this.serializeUrl(storedUrl));
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
this.location.replaceState(this.serializeUrl(this.rawUrlTree));
}
});
});
Expand Down
17 changes: 11 additions & 6 deletions modules/@angular/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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 {UrlHandlingStrategy} from './url_handling_strategy';
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
import {flatten} from './utils/collection';

Expand Down Expand Up @@ -55,7 +56,7 @@ export const ROUTER_PROVIDERS: Provider[] = [
useFactory: setupRouter,
deps: [
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
Compiler, ROUTES, ROUTER_CONFIGURATION
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
]
},
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
Expand Down Expand Up @@ -236,24 +237,28 @@ export interface ExtraOptions {
export function setupRouter(
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
config: Route[][], opts: ExtraOptions = {}) {
const r = new Router(
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) {
const router = new Router(
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));

if (urlHandlingStrategy) {
router.urlHandlingStrategy = urlHandlingStrategy;
}

if (opts.errorHandler) {
r.errorHandler = opts.errorHandler;
router.errorHandler = opts.errorHandler;
}

if (opts.enableTracing) {
r.events.subscribe(e => {
router.events.subscribe(e => {
console.group(`Router Event: ${(<any>e.constructor).name}`);
console.log(e.toString());
console.log(e);
console.groupEnd();
});
}

return r;
return router;
}

export function rootRoute(router: Router): ActivatedRoute {
Expand Down
46 changes: 46 additions & 0 deletions modules/@angular/router/src/url_handling_strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {UrlTree} from './url_tree';

/**
* @whatItDoes Provides a way to migrate Angular 1 applications to Angular 2.
*
* @experimental
*/
export abstract class UrlHandlingStrategy {
/**
* Tells the router if this URL should be processed.
*
* When it returns true, the router will execute the regular navigation.
* When it returns false, the router will set the router state to an empty state.
* As a result, all the active components will be destroyed.
*
*/
abstract shouldProcessUrl(url: UrlTree): boolean;

/**
* Extracts the part of the URL that should be handled by the router.
* The rest of the URL will remain untouched.
*/
abstract extract(url: UrlTree): UrlTree;

/**
* Merges the URL fragment with the rest of the URL.
*/
abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree;
}

/**
* @experimental
*/
export class DefaultUrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: UrlTree): boolean { return true; }
extract(url: UrlTree): UrlTree { return url; }
merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree { return newUrlPart; }
}
Loading

0 comments on commit 315042a

Please sign in to comment.