Skip to content
Closed
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
4,183 changes: 0 additions & 4,183 deletions aio/content/guide/router.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions goldens/public-api/router/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export declare type Event = RouterEvent | RouteConfigLoadStart | RouteConfigLoad

export declare interface ExtraOptions {
anchorScrolling?: 'disabled' | 'enabled';
cascade?: true | false | 'legacy' | 'cascade';
enableTracing?: boolean;
errorHandler?: ErrorHandler;
initialNavigation?: InitialNavigation;
Expand Down Expand Up @@ -323,6 +324,7 @@ export declare class RouteConfigLoadStart {
}

export declare class Router {
cascade: true | false;
config: Routes;
errorHandler: ErrorHandler;
readonly events: Observable<Event>;
Expand Down
38 changes: 24 additions & 14 deletions packages/router/src/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {Injector, NgModuleRef} from '@angular/core';
import {EmptyError, from, Observable, Observer, of} from 'rxjs';
import {catchError, concatAll, every, first, map, mergeMap} from 'rxjs/operators';
import {catchError, concatAll, every, first, last, map, mergeMap} from 'rxjs/operators';

import {LoadedRouterConfig, Route, Routes} from './config';
import {CanLoadFn} from './interfaces';
Expand Down Expand Up @@ -60,8 +60,9 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
*/
export function applyRedirects(
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
urlTree: UrlTree, config: Routes): Observable<UrlTree> {
return new ApplyRedirects(moduleInjector, configLoader, urlSerializer, urlTree, config).apply();
urlTree: UrlTree, config: Routes, cascade: boolean = false): Observable<UrlTree> {
return new ApplyRedirects(moduleInjector, configLoader, urlSerializer, urlTree, config, cascade)
.apply();
}

class ApplyRedirects {
Expand All @@ -70,7 +71,8 @@ class ApplyRedirects {

constructor(
moduleInjector: Injector, private configLoader: RouterConfigLoader,
private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes) {
private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes,
private cascade: boolean = false) {
this.ngModule = moduleInjector.get(NgModuleRef);
}

Expand Down Expand Up @@ -147,7 +149,7 @@ class ApplyRedirects {
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[],
segments: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
return of(...routes).pipe(
let route$ = of(...routes).pipe(
map((r: any) => {
const expanded$ = this.expandSegmentAgainstRoute(
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
Expand All @@ -160,15 +162,23 @@ class ApplyRedirects {
throw e;
}));
}),
concatAll(), first((s: any) => !!s), catchError((e: any, _: any) => {
if (e instanceof EmptyError || e.name === 'EmptyError') {
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
return of(new UrlSegmentGroup([], {}));
}
throw new NoMatch(segmentGroup);
}
throw e;
}));
concatAll());

if (this.cascade) {
// all matching lazy-loadable routes (loadChildren) must be expanded
const first$: any = route$.pipe(first((s: any) => !!s));
route$ = route$.pipe(last((s: any) => !!s), mergeMap(() => first$, 1));
}

return route$.pipe(first((s: any) => !!s), catchError((e: any, _: any) => {
if (e instanceof EmptyError || e.name === 'EmptyError') {
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
return of(new UrlSegmentGroup([], {}));
}
throw new NoMatch(segmentGroup);
}
throw e;
}));
}

private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
Expand Down
5 changes: 3 additions & 2 deletions packages/router/src/operators/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import {UrlSerializer} from '../url_tree';

export function applyRedirects(
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
config: Routes): MonoTypeOperatorFunction<NavigationTransition> {
config: Routes, cascade: boolean = false): MonoTypeOperatorFunction<NavigationTransition> {
return function(source: Observable<NavigationTransition>) {
return source.pipe(switchMap(
t => applyRedirectsFn(moduleInjector, configLoader, urlSerializer, t.extractedUrl, config)
t => applyRedirectsFn(
moduleInjector, configLoader, urlSerializer, t.extractedUrl, config, cascade)
.pipe(map(urlAfterRedirects => ({...t, urlAfterRedirects})))));
};
}
19 changes: 11 additions & 8 deletions packages/router/src/operators/recognize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@
*/

import {Type} from '@angular/core';
import {MonoTypeOperatorFunction, Observable} from 'rxjs';
import {concat, from, MonoTypeOperatorFunction, Observable} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';

import {Route} from '../config';
import {recognize as recognizeFn} from '../recognize';
import {IRecognizeStrategy, recognize as recognizeFn} from '../recognize';
import {NavigationTransition} from '../router';
import {UrlTree} from '../url_tree';

export function recognize(
rootComponentType: Type<any>|null, config: Route[], serializer: (url: UrlTree) => string,
paramsInheritanceStrategy: 'emptyOnly'|'always',
relativeLinkResolution: 'legacy'|'corrected'): MonoTypeOperatorFunction<NavigationTransition> {
paramsInheritanceStrategy: 'emptyOnly'|'always', relativeLinkResolution: 'legacy'|'corrected',
matchingStrategy: IRecognizeStrategy): MonoTypeOperatorFunction<NavigationTransition> {
return function(source: Observable<NavigationTransition>) {
return source.pipe(mergeMap(
t => recognizeFn(
rootComponentType, config, t.urlAfterRedirects, serializer(t.urlAfterRedirects),
paramsInheritanceStrategy, relativeLinkResolution)
.pipe(map(targetSnapshot => ({...t, targetSnapshot})))));
t =>
concat(
recognizeFn(
rootComponentType, config, t.urlAfterRedirects, serializer(t.urlAfterRedirects),
paramsInheritanceStrategy, relativeLinkResolution, matchingStrategy),
from([null as any]))
.pipe(map(targetSnapshot => ({...t, targetSnapshot})))));
};
}
131 changes: 117 additions & 14 deletions packages/router/src/recognize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {Type} from '@angular/core';
import {Observable, Observer, of} from 'rxjs';
import {from, Observable, Observer, of} from 'rxjs';

import {Data, ResolveData, Route, Routes} from './config';
import {ActivatedRouteSnapshot, inheritedParamsDataResolve, ParamsInheritanceStrategy, RouterStateSnapshot} from './router_state';
Expand All @@ -18,39 +18,140 @@ import {TreeNode} from './utils/tree';

class NoMatch {}

/**
* @description
*
* Signature for implementing a route matching algorithm.
*
* @internal
*/
export interface IRecognizeStrategy {
/**
* Indicate whether an outlet/route should be visited or skipped
* based on the implemented strategy and previous occurance of visits.
*/
visit(outlet: string, route: Route): boolean;
Comment thread
matthewerwin marked this conversation as resolved.
Resolved

/**
* Signal when no additional routes/outlets were visited since
* the last iteration.
*/
hasNewVisits(): boolean;
Comment thread
matthewerwin marked this conversation as resolved.
Resolved
}

/**
* @description
*
* A helper class utilized by recognize() to iteratively track by outlet which routes
* have already been visited in a prior iteration in order to identify all matching routes.
*
* Tracks by outlet & route in order to match all possible routes including
* 'route-x(outlet:route-y)', '**', and others which can be used if guards block
* higher priority routes.
*
* @internal
*/
export class RecognizeAllStrategy implements IRecognizeStrategy {
private visited: {[outlet: string]: Route[]} = {[PRIMARY_OUTLET]: []};
private newVisits: number = 0;

/**
* Determine if a route should be evaulated by recognize() for
* the an outlet and route. Returns `true` if the route has not
* been previously evaulated for the given `outlet`
*
* @internal
*/
public visit(outlet: string, route: Route): boolean {
const set = (this.visited[outlet] = this.visited[outlet] || []);
if (set.indexOf(route) !== -1) {
return !this.visitedAllChildren(outlet, route);
}
set.push(route);
this.newVisits++;
return true;
}

private visitedAllChildren(outlet: string, route: Route): boolean {
if (this.visited[outlet].indexOf(route) === -1) return false;
return getChildConfig(route).every(childRoute => this.visitedAllChildren(outlet, childRoute));
}

/**
* Returns `true` if new routes have been visited since the last call
* to this method.
*
* @internal
*/
public hasNewVisits(): boolean {
const newVisits: number = this.newVisits;
this.newVisits = 0;
return newVisits > 0;
}
}

/**
* @description
*
* A helper class utilized by recognize() to locate only the first match. This
* is the default behavior and is backwards compatible.
*
* @internal
*/
export class RecognizeFirstStrategy implements IRecognizeStrategy {
public visit(outlet: string, route: Route) {
return true;
}
public hasNewVisits() {
return false;
}
}

export function recognize(
rootComponentType: Type<any>|null, config: Routes, urlTree: UrlTree, url: string,
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly',
relativeLinkResolution: 'legacy'|'corrected' = 'legacy'): Observable<RouterStateSnapshot> {
relativeLinkResolution: 'legacy'|'corrected' = 'legacy',
matchingStrategy: IRecognizeStrategy =
new RecognizeFirstStrategy()): Observable<RouterStateSnapshot> {
return new Recognizer(
rootComponentType, config, urlTree, url, paramsInheritanceStrategy,
relativeLinkResolution)
relativeLinkResolution, matchingStrategy)
.recognize();
}

class Recognizer {
constructor(
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy,
private relativeLinkResolution: 'legacy'|'corrected') {}
private relativeLinkResolution: 'legacy'|'corrected',
private matchingStrategy: IRecognizeStrategy = new RecognizeFirstStrategy()) {}


recognize(): Observable<RouterStateSnapshot> {
try {
const rootSegmentGroup =
split(this.urlTree.root, [], [], this.config, this.relativeLinkResolution).segmentGroup;

const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);

const root = new ActivatedRouteSnapshot(
[], Object.freeze({}), Object.freeze({...this.urlTree.queryParams}),
this.urlTree.fragment!, {}, PRIMARY_OUTLET, this.rootComponentType, null,
this.urlTree.root, -1, {});

const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
const routeState = new RouterStateSnapshot(this.url, rootNode);
this.inheritParamsAndData(routeState._root);
return of(routeState);

const recognizedRoutes: RouterStateSnapshot[] = [];
do {
try {
const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);
const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
const routeState = new RouterStateSnapshot(this.url, rootNode);
this.inheritParamsAndData(routeState._root);
recognizedRoutes.push(routeState);
} catch (e) {
if (!(e instanceof NoMatch)) {
throw e;
}
}
} while (this.matchingStrategy.hasNewVisits());
return from(recognizedRoutes);
} catch (e) {
return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) => obs.error(e));
Expand All @@ -72,7 +173,6 @@ class Recognizer {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return this.processChildren(config, segmentGroup);
}

return this.processSegment(config, segmentGroup, segmentGroup.segments, outlet);
}

Expand All @@ -90,6 +190,9 @@ class Recognizer {
outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
for (const r of config) {
try {
if (!this.matchingStrategy.visit(outlet, r)) {
continue;
}
return this.processSegmentAgainstRoute(r, segmentGroup, segments, outlet);
} catch (e) {
if (!(e instanceof NoMatch)) throw e;
Expand Down Expand Up @@ -168,8 +271,8 @@ function getChildConfig(route: Route): Route[] {
return route.children;
}

if (route.loadChildren) {
return route._loadedConfig!.routes;
if (route.loadChildren && route._loadedConfig) {
return route._loadedConfig.routes;
}

return [];
Expand Down
Loading