feat(router): add support for ng1/ng2 migration (#12160)
This commit is contained in:
parent
b0a03fcab3
commit
8b9ab44eee
@ -21,7 +21,6 @@ function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode
|
|||||||
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
|
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
|
||||||
const value = prevState.value;
|
const value = prevState.value;
|
||||||
value._futureSnapshot = curr.value;
|
value._futureSnapshot = curr.value;
|
||||||
|
|
||||||
const children = createOrReuseChildren(curr, prevState);
|
const children = createOrReuseChildren(curr, prevState);
|
||||||
return new TreeNode<ActivatedRoute>(value, children);
|
return new TreeNode<ActivatedRoute>(value, children);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export {RouterOutletMap} from './router_outlet_map';
|
|||||||
export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader';
|
export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader';
|
||||||
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||||
export {PRIMARY_OUTLET, Params} from './shared';
|
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'
|
export * from './private_export'
|
||||||
|
@ -30,6 +30,7 @@ import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
|
|||||||
import {RouterOutletMap} from './router_outlet_map';
|
import {RouterOutletMap} from './router_outlet_map';
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
|
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
|
||||||
import {NavigationCancelingError, PRIMARY_OUTLET, Params} from './shared';
|
import {NavigationCancelingError, PRIMARY_OUTLET, Params} from './shared';
|
||||||
|
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
|
||||||
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
|
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
|
||||||
import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection';
|
import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection';
|
||||||
import {TreeNode} from './utils/tree';
|
import {TreeNode} from './utils/tree';
|
||||||
@ -285,6 +286,9 @@ function defaultErrorHandler(error: any): any {
|
|||||||
*/
|
*/
|
||||||
export class Router {
|
export class Router {
|
||||||
private currentUrlTree: UrlTree;
|
private currentUrlTree: UrlTree;
|
||||||
|
private rawUrlTree: UrlTree;
|
||||||
|
private lastNavigation: UrlTree;
|
||||||
|
|
||||||
private currentRouterState: RouterState;
|
private currentRouterState: RouterState;
|
||||||
private locationSubscription: Subscription;
|
private locationSubscription: Subscription;
|
||||||
private routerEvents: Subject<Event>;
|
private routerEvents: Subject<Event>;
|
||||||
@ -303,6 +307,11 @@ export class Router {
|
|||||||
*/
|
*/
|
||||||
navigated: boolean = false;
|
navigated: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
|
||||||
|
*/
|
||||||
|
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the router service.
|
* Creates the router service.
|
||||||
*/
|
*/
|
||||||
@ -314,6 +323,7 @@ export class Router {
|
|||||||
this.resetConfig(config);
|
this.resetConfig(config);
|
||||||
this.routerEvents = new Subject<Event>();
|
this.routerEvents = new Subject<Event>();
|
||||||
this.currentUrlTree = createEmptyUrlTree();
|
this.currentUrlTree = createEmptyUrlTree();
|
||||||
|
this.rawUrlTree = this.currentUrlTree;
|
||||||
this.configLoader = new RouterConfigLoader(loader, compiler);
|
this.configLoader = new RouterConfigLoader(loader, compiler);
|
||||||
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
|
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
|
||||||
}
|
}
|
||||||
@ -344,12 +354,20 @@ export class Router {
|
|||||||
// Zone.current.wrap is needed because of the issue with RxJS scheduler,
|
// Zone.current.wrap is needed because of the issue with RxJS scheduler,
|
||||||
// which does not work properly with zone.js in IE and Safari
|
// which does not work properly with zone.js in IE and Safari
|
||||||
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
||||||
const tree = this.urlSerializer.parse(change['url']);
|
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
||||||
// we fire multiple events for a single URL change
|
const tree = this.urlHandlingStrategy.extract(rawUrlTree);
|
||||||
// we should navigate only once
|
|
||||||
return this.currentUrlTree.toString() !== tree.toString() ?
|
|
||||||
this.scheduleNavigation(tree, {skipLocationChange: change['pop'], replaceUrl: true}) :
|
setTimeout(() => {
|
||||||
null;
|
// 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);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,10 +488,10 @@ export class Router {
|
|||||||
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
|
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
if (url instanceof UrlTree) {
|
if (url instanceof UrlTree) {
|
||||||
return this.scheduleNavigation(url, extras);
|
return this.scheduleNavigation(this.rawUrlTree, url, extras);
|
||||||
} else {
|
} else {
|
||||||
const urlTree = this.urlSerializer.parse(url);
|
const urlTree = this.urlSerializer.parse(url);
|
||||||
return this.scheduleNavigation(urlTree, extras);
|
return this.scheduleNavigation(this.rawUrlTree, urlTree, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,7 +518,7 @@ export class Router {
|
|||||||
*/
|
*/
|
||||||
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
|
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
return this.scheduleNavigation(this.createUrlTree(commands, extras), extras);
|
return this.scheduleNavigation(this.rawUrlTree, this.createUrlTree(commands, extras), extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -525,16 +543,34 @@ export class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNavigation(url: UrlTree, extras: NavigationExtras): Promise<boolean> {
|
private scheduleNavigation(rawUrl: UrlTree, url: UrlTree, extras: NavigationExtras):
|
||||||
const id = ++this.navigationId;
|
Promise<boolean> {
|
||||||
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
if (this.urlHandlingStrategy.shouldProcessUrl(url)) {
|
||||||
return Promise.resolve().then(
|
const id = ++this.navigationId;
|
||||||
(_) => this.runNavigate(url, extras.skipLocationChange, extras.replaceUrl, id));
|
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(
|
private runNavigate(
|
||||||
url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
|
rawUrl: UrlTree, url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
|
||||||
id: number): Promise<boolean> {
|
id: number, precreatedState: RouterState): Promise<boolean> {
|
||||||
if (id !== this.navigationId) {
|
if (id !== this.navigationId) {
|
||||||
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
||||||
this.routerEvents.next(new NavigationCancel(
|
this.routerEvents.next(new NavigationCancel(
|
||||||
@ -553,23 +589,33 @@ export class Router {
|
|||||||
const storedState = this.currentRouterState;
|
const storedState = this.currentRouterState;
|
||||||
const storedUrl = this.currentUrlTree;
|
const storedUrl = this.currentUrlTree;
|
||||||
|
|
||||||
const redirectsApplied$ = applyRedirects(this.injector, this.configLoader, url, this.config);
|
let routerState$: any;
|
||||||
|
|
||||||
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
|
if (!precreatedState) {
|
||||||
appliedUrl = u;
|
const redirectsApplied$ =
|
||||||
return recognize(
|
applyRedirects(this.injector, this.configLoader, url, this.config);
|
||||||
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
|
|
||||||
});
|
|
||||||
|
|
||||||
const emitRecognzied$ = map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
|
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
|
||||||
this.routerEvents.next(new RoutesRecognized(
|
appliedUrl = u;
|
||||||
id, this.serializeUrl(url), this.serializeUrl(appliedUrl), newRouterStateSnapshot));
|
return recognize(
|
||||||
return newRouterStateSnapshot;
|
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
|
||||||
});
|
});
|
||||||
|
|
||||||
const routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => {
|
const emitRecognzied$ =
|
||||||
return createRouterState(routerStateSnapshot, this.currentRouterState);
|
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) => {
|
const preactivation$ = map.call(routerState$, (newState: RouterState) => {
|
||||||
state = newState;
|
state = newState;
|
||||||
@ -595,11 +641,14 @@ export class Router {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastNavigation = appliedUrl;
|
||||||
this.currentUrlTree = appliedUrl;
|
this.currentUrlTree = appliedUrl;
|
||||||
|
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
|
||||||
|
|
||||||
this.currentRouterState = state;
|
this.currentRouterState = state;
|
||||||
|
|
||||||
if (!shouldPreventPushState) {
|
if (!shouldPreventPushState) {
|
||||||
let path = this.urlSerializer.serialize(appliedUrl);
|
let path = this.urlSerializer.serialize(this.rawUrlTree);
|
||||||
if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) {
|
if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) {
|
||||||
this.location.replaceState(path);
|
this.location.replaceState(path);
|
||||||
} else {
|
} else {
|
||||||
@ -641,7 +690,8 @@ export class Router {
|
|||||||
if (id === this.navigationId) {
|
if (id === this.navigationId) {
|
||||||
this.currentRouterState = storedState;
|
this.currentRouterState = storedState;
|
||||||
this.currentUrlTree = storedUrl;
|
this.currentUrlTree = storedUrl;
|
||||||
this.location.replaceState(this.serializeUrl(storedUrl));
|
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
|
||||||
|
this.location.replaceState(this.serializeUrl(this.rawUrlTree));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ import {ROUTES} from './router_config_loader';
|
|||||||
import {RouterOutletMap} from './router_outlet_map';
|
import {RouterOutletMap} from './router_outlet_map';
|
||||||
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||||
import {ActivatedRoute} from './router_state';
|
import {ActivatedRoute} from './router_state';
|
||||||
|
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||||
import {flatten} from './utils/collection';
|
import {flatten} from './utils/collection';
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ export const ROUTER_PROVIDERS: Provider[] = [
|
|||||||
useFactory: setupRouter,
|
useFactory: setupRouter,
|
||||||
deps: [
|
deps: [
|
||||||
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
||||||
Compiler, ROUTES, ROUTER_CONFIGURATION
|
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
|
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
|
||||||
@ -236,16 +237,20 @@ export interface ExtraOptions {
|
|||||||
export function setupRouter(
|
export function setupRouter(
|
||||||
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
||||||
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
||||||
config: Route[][], opts: ExtraOptions = {}) {
|
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) {
|
||||||
const r = new Router(
|
const router = new Router(
|
||||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
||||||
|
|
||||||
|
if (urlHandlingStrategy) {
|
||||||
|
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.errorHandler) {
|
if (opts.errorHandler) {
|
||||||
r.errorHandler = opts.errorHandler;
|
router.errorHandler = opts.errorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.enableTracing) {
|
if (opts.enableTracing) {
|
||||||
r.events.subscribe(e => {
|
router.events.subscribe(e => {
|
||||||
console.group(`Router Event: ${(<any>e.constructor).name}`);
|
console.group(`Router Event: ${(<any>e.constructor).name}`);
|
||||||
console.log(e.toString());
|
console.log(e.toString());
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -253,7 +258,7 @@ export function setupRouter(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rootRoute(router: Router): ActivatedRoute {
|
export function rootRoute(router: Router): ActivatedRoute {
|
||||||
|
46
modules/@angular/router/src/url_handling_strategy.ts
Normal file
46
modules/@angular/router/src/url_handling_strategy.ts
Normal file
@ -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; }
|
||||||
|
}
|
@ -14,8 +14,9 @@ import {Observable} from 'rxjs/Observable';
|
|||||||
import {of } from 'rxjs/observable/of';
|
import {of } from 'rxjs/observable/of';
|
||||||
import {map} from 'rxjs/operator/map';
|
import {map} from 'rxjs/operator/map';
|
||||||
|
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized} from '../index';
|
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
|
||||||
import {RouterPreloader} from '../src/router_preloader';
|
import {RouterPreloader} from '../src/router_preloader';
|
||||||
|
import {forEach} from '../src/utils/collection';
|
||||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||||
|
|
||||||
|
|
||||||
@ -58,7 +59,6 @@ describe('Integration', () => {
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
it('should work when an outlet is in an ngIf (and is removed)', fakeAsync(() => {
|
it('should work when an outlet is in an ngIf (and is removed)', fakeAsync(() => {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'someRoot',
|
selector: 'someRoot',
|
||||||
template: `<div *ngIf="cond"><router-outlet></router-outlet></div>`
|
template: `<div *ngIf="cond"><router-outlet></router-outlet></div>`
|
||||||
@ -87,6 +87,7 @@ describe('Integration', () => {
|
|||||||
let recordedError: any = null;
|
let recordedError: any = null;
|
||||||
router.navigateByUrl('/blank').catch(e => recordedError = e);
|
router.navigateByUrl('/blank').catch(e => recordedError = e);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\'');
|
expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\'');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1757,10 +1758,146 @@ describe('Integration', () => {
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('custom url handling strategies', () => {
|
||||||
|
class CustomUrlHandlingStrategy implements UrlHandlingStrategy {
|
||||||
|
shouldProcessUrl(url: UrlTree): boolean {
|
||||||
|
return url.toString().startsWith('/include') || url.toString() === '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
extract(url: UrlTree): UrlTree {
|
||||||
|
const oldRoot = url.root;
|
||||||
|
const root = new UrlSegmentGroup(
|
||||||
|
oldRoot.segments, {[PRIMARY_OUTLET]: oldRoot.children[PRIMARY_OUTLET]});
|
||||||
|
return new UrlTree(root, url.queryParams, url.fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree {
|
||||||
|
const oldRoot = newUrlPart.root;
|
||||||
|
|
||||||
|
const children: any = {};
|
||||||
|
if (oldRoot.children[PRIMARY_OUTLET]) {
|
||||||
|
children[PRIMARY_OUTLET] = oldRoot.children[PRIMARY_OUTLET];
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(wholeUrl.root.children, (v: any, k: any) => {
|
||||||
|
if (k !== PRIMARY_OUTLET) {
|
||||||
|
children[k] = v;
|
||||||
|
}
|
||||||
|
v.parent = this;
|
||||||
|
});
|
||||||
|
const root = new UrlSegmentGroup(oldRoot.segments, children);
|
||||||
|
return new UrlTree(root, newUrlPart.queryParams, newUrlPart.fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{providers: [{provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}]});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([{
|
||||||
|
path: 'include',
|
||||||
|
component: TeamCmp,
|
||||||
|
children: [
|
||||||
|
{path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp}
|
||||||
|
]
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let events: any[] = [];
|
||||||
|
router.events.subscribe(e => events.push(e));
|
||||||
|
|
||||||
|
// supported URL
|
||||||
|
router.navigateByUrl('/include/user/kate');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/include/user/kate');
|
||||||
|
expectEvents(events, [
|
||||||
|
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
|
||||||
|
[NavigationEnd, '/include/user/kate']
|
||||||
|
]);
|
||||||
|
expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]');
|
||||||
|
events.splice(0);
|
||||||
|
|
||||||
|
// unsupported URL
|
||||||
|
router.navigateByUrl('/exclude/one');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/exclude/one');
|
||||||
|
expect(Object.keys(router.routerState.root.children).length).toEqual(0);
|
||||||
|
expect(fixture.nativeElement).toHaveText('');
|
||||||
|
expectEvents(
|
||||||
|
events, [[NavigationStart, '/exclude/one'], [NavigationEnd, '/exclude/one']]);
|
||||||
|
events.splice(0);
|
||||||
|
|
||||||
|
// another unsupported URL
|
||||||
|
location.go('/exclude/two');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/exclude/two');
|
||||||
|
expectEvents(events, []);
|
||||||
|
|
||||||
|
// back to a supported URL
|
||||||
|
location.go('/include/simple');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/include/simple');
|
||||||
|
expect(fixture.nativeElement).toHaveText('team [ simple, right: ]');
|
||||||
|
|
||||||
|
expectEvents(events, [
|
||||||
|
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
|
||||||
|
[NavigationEnd, '/include/simple']
|
||||||
|
]);
|
||||||
|
})));
|
||||||
|
|
||||||
|
it('should handle the case when the router takes only the primary url',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([{
|
||||||
|
path: 'include',
|
||||||
|
component: TeamCmp,
|
||||||
|
children: [
|
||||||
|
{path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp}
|
||||||
|
]
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let events: any[] = [];
|
||||||
|
router.events.subscribe(e => events.push(e));
|
||||||
|
|
||||||
|
location.go('/include/user/kate(aux:excluded)');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/include/user/kate(aux:excluded)');
|
||||||
|
expectEvents(events, [
|
||||||
|
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
|
||||||
|
[NavigationEnd, '/include/user/kate']
|
||||||
|
]);
|
||||||
|
events.splice(0);
|
||||||
|
|
||||||
|
location.go('/include/user/kate(aux:excluded2)');
|
||||||
|
advance(fixture);
|
||||||
|
expectEvents(events, []);
|
||||||
|
|
||||||
|
router.navigateByUrl('/include/simple');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/include/simple(aux:excluded2)');
|
||||||
|
expectEvents(events, [
|
||||||
|
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
|
||||||
|
[NavigationEnd, '/include/simple']
|
||||||
|
]);
|
||||||
|
})));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectEvents(events: Event[], pairs: any[]) {
|
function expectEvents(events: Event[], pairs: any[]) {
|
||||||
|
expect(events.length).toEqual(pairs.length);
|
||||||
for (let i = 0; i < events.length; ++i) {
|
for (let i = 0; i < events.length; ++i) {
|
||||||
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);
|
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);
|
||||||
expect((<any>events[i]).url).toBe(pairs[i][1]);
|
expect((<any>events[i]).url).toBe(pairs[i][1]);
|
||||||
@ -1771,11 +1908,7 @@ function expectEvents(events: Event[], pairs: any[]) {
|
|||||||
class StringLinkCmp {
|
class StringLinkCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({selector: 'link-cmp', template: `<button routerLink="/team/33/simple">link</button>`})
|
||||||
selector: 'link-cmp',
|
|
||||||
template: `<button routerLink
|
|
||||||
="/team/33/simple">link</button>`
|
|
||||||
})
|
|
||||||
class StringLinkButtonCmp {
|
class StringLinkButtonCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,13 @@
|
|||||||
|
|
||||||
import {Location, LocationStrategy} from '@angular/common';
|
import {Location, LocationStrategy} from '@angular/common';
|
||||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||||
import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
|
import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader, Optional} from '@angular/core';
|
||||||
import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlSerializer, provideRoutes} from '@angular/router';
|
import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes} from '@angular/router';
|
||||||
|
|
||||||
import {ROUTER_PROVIDERS, ROUTES, flatten} from './private_import_router';
|
import {ROUTER_PROVIDERS, ROUTES, flatten} from './private_import_router';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Allows to simulate the loading of ng modules in tests.
|
* @whatItDoes Allows to simulate the loading of ng modules in tests.
|
||||||
*
|
*
|
||||||
@ -68,9 +69,14 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader {
|
|||||||
*/
|
*/
|
||||||
export function setupTestingRouter(
|
export function setupTestingRouter(
|
||||||
urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location,
|
urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location,
|
||||||
loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][]) {
|
loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][],
|
||||||
return new Router(
|
urlHandlingStrategy?: UrlHandlingStrategy) {
|
||||||
|
const router = new Router(
|
||||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(routes));
|
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(routes));
|
||||||
|
if (urlHandlingStrategy) {
|
||||||
|
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||||
|
}
|
||||||
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,7 +113,8 @@ export function setupTestingRouter(
|
|||||||
provide: Router,
|
provide: Router,
|
||||||
useFactory: setupTestingRouter,
|
useFactory: setupTestingRouter,
|
||||||
deps: [
|
deps: [
|
||||||
UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES
|
UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES,
|
||||||
|
[UrlHandlingStrategy, new Optional()]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([])
|
{provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([])
|
||||||
|
25
tools/public_api_guard/router/index.d.ts
vendored
25
tools/public_api_guard/router/index.d.ts
vendored
@ -202,6 +202,7 @@ export declare class Router {
|
|||||||
navigated: boolean;
|
navigated: boolean;
|
||||||
routerState: RouterState;
|
routerState: RouterState;
|
||||||
url: string;
|
url: string;
|
||||||
|
urlHandlingStrategy: UrlHandlingStrategy;
|
||||||
constructor(rootComponentType: Type<any>, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes);
|
constructor(rootComponentType: Type<any>, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes);
|
||||||
createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree;
|
createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
@ -322,6 +323,13 @@ export declare class RoutesRecognized {
|
|||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare abstract class UrlHandlingStrategy {
|
||||||
|
abstract extract(url: UrlTree): UrlTree;
|
||||||
|
abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree;
|
||||||
|
abstract shouldProcessUrl(url: UrlTree): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare class UrlSegment {
|
export declare class UrlSegment {
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -336,6 +344,23 @@ export declare class UrlSegment {
|
|||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @stable */
|
||||||
|
export declare class UrlSegmentGroup {
|
||||||
|
children: {
|
||||||
|
[key: string]: UrlSegmentGroup;
|
||||||
|
};
|
||||||
|
numberOfChildren: number;
|
||||||
|
parent: UrlSegmentGroup;
|
||||||
|
segments: UrlSegment[];
|
||||||
|
constructor(
|
||||||
|
segments: UrlSegment[],
|
||||||
|
children: {
|
||||||
|
[key: string]: UrlSegmentGroup;
|
||||||
|
});
|
||||||
|
hasChildren(): boolean;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare abstract class UrlSerializer {
|
export declare abstract class UrlSerializer {
|
||||||
abstract parse(url: string): UrlTree;
|
abstract parse(url: string): UrlTree;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user