mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor: migrate router to prettier formatting (#54318)
Migrate formatting to prettier for router from clang-format PR Close #54318
This commit is contained in:
parent
d02fcb1ab4
commit
b857aafcb9
79 changed files with 13550 additions and 10967 deletions
|
|
@ -20,6 +20,7 @@ export const format: FormatConfig = {
|
|||
'packages/examples/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
'packages/private/**/*.{js,ts}',
|
||||
'packages/router/**/*.{js,ts}',
|
||||
'packages/service-worker/**/*.{js,ts}',
|
||||
'packages/upgrade/**/*.{js,ts}',
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ export const format: FormatConfig = {
|
|||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
//'**/*.{js,ts}',
|
||||
'**/*.{js,ts}',
|
||||
// TODO: burn down format failures and remove aio and integration exceptions.
|
||||
'!aio/**',
|
||||
'!integration/**',
|
||||
|
|
@ -70,6 +71,7 @@ export const format: FormatConfig = {
|
|||
'!packages/examples/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
'!packages/private/**/*.{js,ts}',
|
||||
'!packages/router/**/*.{js,ts}',
|
||||
'!packages/service-worker/**/*.{js,ts}',
|
||||
'!packages/upgrade/**/*.{js,ts}',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"tabs": false,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"singleQuote": true,
|
||||
"semicolon": true,
|
||||
"quoteProps": "preserve",
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ import {navigationCancelingError} from './navigation_canceling_error';
|
|||
import {Params, PRIMARY_OUTLET} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
|
||||
|
||||
export class NoMatch {
|
||||
public segmentGroup: UrlSegmentGroup|null;
|
||||
public segmentGroup: UrlSegmentGroup | null;
|
||||
|
||||
constructor(segmentGroup?: UrlSegmentGroup) {
|
||||
this.segmentGroup = segmentGroup || null;
|
||||
|
|
@ -40,23 +39,30 @@ export function absoluteRedirect(newTree: UrlTree): Observable<any> {
|
|||
}
|
||||
|
||||
export function namedOutletsRedirect(redirectTo: string): Observable<any> {
|
||||
return throwError(new RuntimeError(
|
||||
return throwError(
|
||||
new RuntimeError(
|
||||
RuntimeErrorCode.NAMED_OUTLET_REDIRECT,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`));
|
||||
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
|
||||
return throwError(navigationCancelingError(
|
||||
return throwError(
|
||||
navigationCancelingError(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Cannot load children because the guard of the route "path: '${
|
||||
route.path}'" returned false`,
|
||||
NavigationCancellationCode.GuardRejected));
|
||||
`Cannot load children because the guard of the route "path: '${route.path}'" returned false`,
|
||||
NavigationCancellationCode.GuardRejected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export class ApplyRedirects {
|
||||
constructor(private urlSerializer: UrlSerializer, private urlTree: UrlTree) {}
|
||||
constructor(
|
||||
private urlSerializer: UrlSerializer,
|
||||
private urlTree: UrlTree,
|
||||
) {}
|
||||
|
||||
lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
|
||||
let res: UrlSegment[] = [];
|
||||
|
|
@ -76,9 +82,16 @@ export class ApplyRedirects {
|
|||
}
|
||||
|
||||
applyRedirectCommands(
|
||||
segments: UrlSegment[], redirectTo: string, posParams: {[k: string]: UrlSegment}): UrlTree {
|
||||
segments: UrlSegment[],
|
||||
redirectTo: string,
|
||||
posParams: {[k: string]: UrlSegment},
|
||||
): UrlTree {
|
||||
const newTree = this.applyRedirectCreateUrlTree(
|
||||
redirectTo, this.urlSerializer.parse(redirectTo), segments, posParams);
|
||||
redirectTo,
|
||||
this.urlSerializer.parse(redirectTo),
|
||||
segments,
|
||||
posParams,
|
||||
);
|
||||
if (redirectTo.startsWith('/')) {
|
||||
throw new AbsoluteRedirect(newTree);
|
||||
}
|
||||
|
|
@ -86,12 +99,17 @@ export class ApplyRedirects {
|
|||
}
|
||||
|
||||
applyRedirectCreateUrlTree(
|
||||
redirectTo: string, urlTree: UrlTree, segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlTree {
|
||||
redirectTo: string,
|
||||
urlTree: UrlTree,
|
||||
segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment},
|
||||
): UrlTree {
|
||||
const newRoot = this.createSegmentGroup(redirectTo, urlTree.root, segments, posParams);
|
||||
return new UrlTree(
|
||||
newRoot, this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
|
||||
urlTree.fragment);
|
||||
newRoot,
|
||||
this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
|
||||
urlTree.fragment,
|
||||
);
|
||||
}
|
||||
|
||||
createQueryParams(redirectToParams: Params, actualParams: Params): Params {
|
||||
|
|
@ -109,8 +127,11 @@ export class ApplyRedirects {
|
|||
}
|
||||
|
||||
createSegmentGroup(
|
||||
redirectTo: string, group: UrlSegmentGroup, segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegmentGroup {
|
||||
redirectTo: string,
|
||||
group: UrlSegmentGroup,
|
||||
segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment},
|
||||
): UrlSegmentGroup {
|
||||
const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams);
|
||||
|
||||
let children: {[n: string]: UrlSegmentGroup} = {};
|
||||
|
|
@ -122,22 +143,30 @@ export class ApplyRedirects {
|
|||
}
|
||||
|
||||
createSegments(
|
||||
redirectTo: string, redirectToSegments: UrlSegment[], actualSegments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
|
||||
return redirectToSegments.map(
|
||||
s => s.path.startsWith(':') ? this.findPosParam(redirectTo, s, posParams) :
|
||||
this.findOrReturn(s, actualSegments));
|
||||
redirectTo: string,
|
||||
redirectToSegments: UrlSegment[],
|
||||
actualSegments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment},
|
||||
): UrlSegment[] {
|
||||
return redirectToSegments.map((s) =>
|
||||
s.path.startsWith(':')
|
||||
? this.findPosParam(redirectTo, s, posParams)
|
||||
: this.findOrReturn(s, actualSegments),
|
||||
);
|
||||
}
|
||||
|
||||
findPosParam(
|
||||
redirectTo: string, redirectToUrlSegment: UrlSegment,
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegment {
|
||||
redirectTo: string,
|
||||
redirectToUrlSegment: UrlSegment,
|
||||
posParams: {[k: string]: UrlSegment},
|
||||
): UrlSegment {
|
||||
const pos = posParams[redirectToUrlSegment.path.substring(1)];
|
||||
if (!pos)
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.MISSING_REDIRECT,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`);
|
||||
RuntimeErrorCode.MISSING_REDIRECT,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`,
|
||||
);
|
||||
return pos;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {RouterOutlet} from '../directives/router_outlet';
|
|||
imports: [RouterOutlet],
|
||||
standalone: true,
|
||||
})
|
||||
export class ɵEmptyOutletComponent {
|
||||
}
|
||||
export class ɵEmptyOutletComponent {}
|
||||
|
||||
export {ɵEmptyOutletComponent as EmptyOutletComponent};
|
||||
|
|
|
|||
|
|
@ -9,19 +9,28 @@
|
|||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterState,
|
||||
RouterStateSnapshot,
|
||||
} from './router_state';
|
||||
import {TreeNode} from './utils/tree';
|
||||
|
||||
export function createRouterState(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
|
||||
prevState: RouterState): RouterState {
|
||||
routeReuseStrategy: RouteReuseStrategy,
|
||||
curr: RouterStateSnapshot,
|
||||
prevState: RouterState,
|
||||
): RouterState {
|
||||
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
|
||||
return new RouterState(root, curr);
|
||||
}
|
||||
|
||||
function createNode(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState?: TreeNode<ActivatedRoute>): TreeNode<ActivatedRoute> {
|
||||
routeReuseStrategy: RouteReuseStrategy,
|
||||
curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState?: TreeNode<ActivatedRoute>,
|
||||
): TreeNode<ActivatedRoute> {
|
||||
// reuse an activated route that is currently displayed on the screen
|
||||
if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) {
|
||||
const value = prevState.value;
|
||||
|
|
@ -35,21 +44,23 @@ function createNode(
|
|||
if (detachedRouteHandle !== null) {
|
||||
const tree = (detachedRouteHandle as DetachedRouteHandleInternal).route;
|
||||
tree.value._futureSnapshot = curr.value;
|
||||
tree.children = curr.children.map(c => createNode(routeReuseStrategy, c));
|
||||
tree.children = curr.children.map((c) => createNode(routeReuseStrategy, c));
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
||||
const value = createActivatedRoute(curr.value);
|
||||
const children = curr.children.map(c => createNode(routeReuseStrategy, c));
|
||||
const children = curr.children.map((c) => createNode(routeReuseStrategy, c));
|
||||
return new TreeNode<ActivatedRoute>(value, children);
|
||||
}
|
||||
}
|
||||
|
||||
function createOrReuseChildren(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState: TreeNode<ActivatedRoute>) {
|
||||
return curr.children.map(child => {
|
||||
routeReuseStrategy: RouteReuseStrategy,
|
||||
curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState: TreeNode<ActivatedRoute>,
|
||||
) {
|
||||
return curr.children.map((child) => {
|
||||
for (const p of prevState.children) {
|
||||
if (routeReuseStrategy.shouldReuseRoute(child.value, p.value.snapshot)) {
|
||||
return createNode(routeReuseStrategy, child, p);
|
||||
|
|
@ -61,6 +72,13 @@ function createOrReuseChildren(
|
|||
|
||||
function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
||||
return new ActivatedRoute(
|
||||
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
|
||||
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
|
||||
new BehaviorSubject(c.url),
|
||||
new BehaviorSubject(c.params),
|
||||
new BehaviorSubject(c.queryParams),
|
||||
new BehaviorSubject(c.fragment),
|
||||
new BehaviorSubject(c.data),
|
||||
c.outlet,
|
||||
c.component,
|
||||
c,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {Params, PRIMARY_OUTLET} from './shared';
|
|||
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
|
||||
import {last, shallowEqual} from './utils/collection';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a `UrlTree` relative to an `ActivatedRouteSnapshot`.
|
||||
*
|
||||
|
|
@ -67,17 +66,21 @@ import {last, shallowEqual} from './utils/collection';
|
|||
* ```
|
||||
*/
|
||||
export function createUrlTreeFromSnapshot(
|
||||
relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams: Params|null = null,
|
||||
fragment: string|null = null): UrlTree {
|
||||
relativeTo: ActivatedRouteSnapshot,
|
||||
commands: any[],
|
||||
queryParams: Params | null = null,
|
||||
fragment: string | null = null,
|
||||
): UrlTree {
|
||||
const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo);
|
||||
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment);
|
||||
}
|
||||
|
||||
export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup {
|
||||
let targetGroup: UrlSegmentGroup|undefined;
|
||||
let targetGroup: UrlSegmentGroup | undefined;
|
||||
|
||||
function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot):
|
||||
UrlSegmentGroup {
|
||||
function createSegmentGroupFromRouteRecursive(
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
): UrlSegmentGroup {
|
||||
const childOutlets: {[outlet: string]: UrlSegmentGroup} = {};
|
||||
for (const childSnapshot of currentRoute.children) {
|
||||
const root = createSegmentGroupFromRouteRecursive(childSnapshot);
|
||||
|
|
@ -96,8 +99,11 @@ export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlS
|
|||
}
|
||||
|
||||
export function createUrlTreeFromSegmentGroup(
|
||||
relativeTo: UrlSegmentGroup, commands: any[], queryParams: Params|null,
|
||||
fragment: string|null): UrlTree {
|
||||
relativeTo: UrlSegmentGroup,
|
||||
commands: any[],
|
||||
queryParams: Params | null,
|
||||
fragment: string | null,
|
||||
): UrlTree {
|
||||
let root = relativeTo;
|
||||
while (root.parent) {
|
||||
root = root.parent;
|
||||
|
|
@ -116,9 +122,9 @@ export function createUrlTreeFromSegmentGroup(
|
|||
}
|
||||
|
||||
const position = findStartingPositionForTargetGroup(nav, root, relativeTo);
|
||||
const newSegmentGroup = position.processChildren ?
|
||||
updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands) :
|
||||
updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
|
||||
const newSegmentGroup = position.processChildren
|
||||
? updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands)
|
||||
: updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
|
||||
return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment);
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +141,12 @@ function isCommandWithOutlets(command: any): command is {outlets: {[key: string]
|
|||
}
|
||||
|
||||
function tree(
|
||||
oldRoot: UrlSegmentGroup, oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup,
|
||||
queryParams: Params|null, fragment: string|null): UrlTree {
|
||||
oldRoot: UrlSegmentGroup,
|
||||
oldSegmentGroup: UrlSegmentGroup,
|
||||
newSegmentGroup: UrlSegmentGroup,
|
||||
queryParams: Params | null,
|
||||
fragment: string | null,
|
||||
): UrlTree {
|
||||
let qp: any = {};
|
||||
if (queryParams) {
|
||||
Object.entries(queryParams).forEach(([name, value]) => {
|
||||
|
|
@ -163,8 +173,10 @@ function tree(
|
|||
* value.
|
||||
*/
|
||||
function replaceSegment(
|
||||
current: UrlSegmentGroup, oldSegment: UrlSegmentGroup,
|
||||
newSegment: UrlSegmentGroup): UrlSegmentGroup {
|
||||
current: UrlSegmentGroup,
|
||||
oldSegment: UrlSegmentGroup,
|
||||
newSegment: UrlSegmentGroup,
|
||||
): UrlSegmentGroup {
|
||||
const children: {[key: string]: UrlSegmentGroup} = {};
|
||||
Object.entries(current.children).forEach(([outletName, c]) => {
|
||||
if (c === oldSegment) {
|
||||
|
|
@ -178,20 +190,25 @@ function replaceSegment(
|
|||
|
||||
class Navigation {
|
||||
constructor(
|
||||
public isAbsolute: boolean, public numberOfDoubleDots: number, public commands: any[]) {
|
||||
public isAbsolute: boolean,
|
||||
public numberOfDoubleDots: number,
|
||||
public commands: any[],
|
||||
) {
|
||||
if (isAbsolute && commands.length > 0 && isMatrixParams(commands[0])) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'Root segment cannot have matrix parameters');
|
||||
RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'Root segment cannot have matrix parameters',
|
||||
);
|
||||
}
|
||||
|
||||
const cmdWithOutlet = commands.find(isCommandWithOutlets);
|
||||
if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'{outlets:{}} has to be the last command');
|
||||
RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'{outlets:{}} has to be the last command',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +219,7 @@ class Navigation {
|
|||
|
||||
/** Transforms commands to a normalized `Navigation` */
|
||||
function computeNavigation(commands: any[]): Navigation {
|
||||
if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] === '/') {
|
||||
if (typeof commands[0] === 'string' && commands.length === 1 && commands[0] === '/') {
|
||||
return new Navigation(true, 0, commands);
|
||||
}
|
||||
|
||||
|
|
@ -232,9 +249,11 @@ function computeNavigation(commands: any[]): Navigation {
|
|||
cmd.split('/').forEach((urlPart, partIndex) => {
|
||||
if (partIndex == 0 && urlPart === '.') {
|
||||
// skip './a'
|
||||
} else if (partIndex == 0 && urlPart === '') { // '/a'
|
||||
} else if (partIndex == 0 && urlPart === '') {
|
||||
// '/a'
|
||||
isAbsolute = true;
|
||||
} else if (urlPart === '..') { // '../a'
|
||||
} else if (urlPart === '..') {
|
||||
// '../a'
|
||||
numberOfDoubleDots++;
|
||||
} else if (urlPart != '') {
|
||||
res.push(urlPart);
|
||||
|
|
@ -252,12 +271,17 @@ function computeNavigation(commands: any[]): Navigation {
|
|||
|
||||
class Position {
|
||||
constructor(
|
||||
public segmentGroup: UrlSegmentGroup, public processChildren: boolean, public index: number) {
|
||||
}
|
||||
public segmentGroup: UrlSegmentGroup,
|
||||
public processChildren: boolean,
|
||||
public index: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
function findStartingPositionForTargetGroup(
|
||||
nav: Navigation, root: UrlSegmentGroup, target: UrlSegmentGroup): Position {
|
||||
nav: Navigation,
|
||||
root: UrlSegmentGroup,
|
||||
target: UrlSegmentGroup,
|
||||
): Position {
|
||||
if (nav.isAbsolute) {
|
||||
return new Position(root, true, 0);
|
||||
}
|
||||
|
|
@ -279,7 +303,10 @@ function findStartingPositionForTargetGroup(
|
|||
}
|
||||
|
||||
function createPositionApplyingDoubleDots(
|
||||
group: UrlSegmentGroup, index: number, numberOfDoubleDots: number): Position {
|
||||
group: UrlSegmentGroup,
|
||||
index: number,
|
||||
numberOfDoubleDots: number,
|
||||
): Position {
|
||||
let g = group;
|
||||
let ci = index;
|
||||
let dd = numberOfDoubleDots;
|
||||
|
|
@ -288,15 +315,16 @@ function createPositionApplyingDoubleDots(
|
|||
g = g.parent!;
|
||||
if (!g) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_DOUBLE_DOTS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Invalid number of \'../\'');
|
||||
RuntimeErrorCode.INVALID_DOUBLE_DOTS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && "Invalid number of '../'",
|
||||
);
|
||||
}
|
||||
ci = g.segments.length;
|
||||
}
|
||||
return new Position(g, false, ci - dd);
|
||||
}
|
||||
|
||||
function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} {
|
||||
function getOutlets(commands: unknown[]): {[k: string]: unknown[] | string} {
|
||||
if (isCommandWithOutlets(commands[0])) {
|
||||
return commands[0].outlets;
|
||||
}
|
||||
|
|
@ -305,7 +333,10 @@ function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} {
|
|||
}
|
||||
|
||||
function updateSegmentGroup(
|
||||
segmentGroup: UrlSegmentGroup|undefined, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
segmentGroup: UrlSegmentGroup | undefined,
|
||||
startIndex: number,
|
||||
commands: any[],
|
||||
): UrlSegmentGroup {
|
||||
segmentGroup ??= new UrlSegmentGroup([], {});
|
||||
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return updateSegmentGroupChildren(segmentGroup, startIndex, commands);
|
||||
|
|
@ -315,8 +346,10 @@ function updateSegmentGroup(
|
|||
const slicedCommands = commands.slice(m.commandIndex);
|
||||
if (m.match && m.pathIndex < segmentGroup.segments.length) {
|
||||
const g = new UrlSegmentGroup(segmentGroup.segments.slice(0, m.pathIndex), {});
|
||||
g.children[PRIMARY_OUTLET] =
|
||||
new UrlSegmentGroup(segmentGroup.segments.slice(m.pathIndex), segmentGroup.children);
|
||||
g.children[PRIMARY_OUTLET] = new UrlSegmentGroup(
|
||||
segmentGroup.segments.slice(m.pathIndex),
|
||||
segmentGroup.children,
|
||||
);
|
||||
return updateSegmentGroupChildren(g, 0, slicedCommands);
|
||||
} else if (m.match && slicedCommands.length === 0) {
|
||||
return new UrlSegmentGroup(segmentGroup.segments, {});
|
||||
|
|
@ -330,7 +363,10 @@ function updateSegmentGroup(
|
|||
}
|
||||
|
||||
function updateSegmentGroupChildren(
|
||||
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
startIndex: number,
|
||||
commands: any[],
|
||||
): UrlSegmentGroup {
|
||||
if (commands.length === 0) {
|
||||
return new UrlSegmentGroup(segmentGroup.segments, {});
|
||||
} else {
|
||||
|
|
@ -357,11 +393,17 @@ function updateSegmentGroupChildren(
|
|||
// `UrlSegmentGroup` that is created from an "unsquashed"/expanded `ActivatedRoute` tree.
|
||||
// This code effectively "squashes" empty path primary routes when they have no siblings on
|
||||
// the same level of the tree.
|
||||
if (Object.keys(outlets).some(o => o !== PRIMARY_OUTLET) &&
|
||||
segmentGroup.children[PRIMARY_OUTLET] && segmentGroup.numberOfChildren === 1 &&
|
||||
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0) {
|
||||
const childrenOfEmptyChild =
|
||||
updateSegmentGroupChildren(segmentGroup.children[PRIMARY_OUTLET], startIndex, commands);
|
||||
if (
|
||||
Object.keys(outlets).some((o) => o !== PRIMARY_OUTLET) &&
|
||||
segmentGroup.children[PRIMARY_OUTLET] &&
|
||||
segmentGroup.numberOfChildren === 1 &&
|
||||
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0
|
||||
) {
|
||||
const childrenOfEmptyChild = updateSegmentGroupChildren(
|
||||
segmentGroup.children[PRIMARY_OUTLET],
|
||||
startIndex,
|
||||
commands,
|
||||
);
|
||||
return new UrlSegmentGroup(segmentGroup.segments, childrenOfEmptyChild.children);
|
||||
}
|
||||
|
||||
|
|
@ -400,11 +442,11 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command
|
|||
}
|
||||
const curr = `${command}`;
|
||||
const next =
|
||||
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
|
||||
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
|
||||
|
||||
if (currentPathIndex > 0 && curr === undefined) break;
|
||||
|
||||
if (curr && next && (typeof next === 'object') && next.outlets === undefined) {
|
||||
if (curr && next && typeof next === 'object' && next.outlets === undefined) {
|
||||
if (!compare(curr, next, path)) return noMatch;
|
||||
currentCommandIndex += 2;
|
||||
} else {
|
||||
|
|
@ -418,7 +460,10 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command
|
|||
}
|
||||
|
||||
function createNewSegmentGroup(
|
||||
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
startIndex: number,
|
||||
commands: any[],
|
||||
): UrlSegmentGroup {
|
||||
const paths = segmentGroup.segments.slice(0, startIndex);
|
||||
|
||||
let i = 0;
|
||||
|
|
@ -438,7 +483,7 @@ function createNewSegmentGroup(
|
|||
}
|
||||
|
||||
const curr = isCommandWithOutlets(command) ? command.outlets[PRIMARY_OUTLET] : `${command}`;
|
||||
const next = (i < commands.length - 1) ? commands[i + 1] : null;
|
||||
const next = i < commands.length - 1 ? commands[i + 1] : null;
|
||||
if (curr && next && isMatrixParams(next)) {
|
||||
paths.push(new UrlSegment(curr, stringify(next)));
|
||||
i += 2;
|
||||
|
|
@ -450,8 +495,9 @@ function createNewSegmentGroup(
|
|||
return new UrlSegmentGroup(paths, {});
|
||||
}
|
||||
|
||||
function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}):
|
||||
{[outlet: string]: UrlSegmentGroup} {
|
||||
function createNewSegmentChildren(outlets: {[name: string]: unknown[] | string}): {
|
||||
[outlet: string]: UrlSegmentGroup;
|
||||
} {
|
||||
const children: {[outlet: string]: UrlSegmentGroup} = {};
|
||||
Object.entries(outlets).forEach(([outlet, commands]) => {
|
||||
if (typeof commands === 'string') {
|
||||
|
|
@ -466,7 +512,7 @@ function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}):
|
|||
|
||||
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
||||
const res: {[key: string]: string} = {};
|
||||
Object.entries(params).forEach(([k, v]) => res[k] = `${v}`);
|
||||
Object.entries(params).forEach(([k, v]) => (res[k] = `${v}`));
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,20 @@
|
|||
*/
|
||||
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
import {Attribute, booleanAttribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ɵɵsanitizeUrlOrResourceUrl} from '@angular/core';
|
||||
import {
|
||||
Attribute,
|
||||
booleanAttribute,
|
||||
Directive,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ɵɵsanitizeUrlOrResourceUrl,
|
||||
} from '@angular/core';
|
||||
import {Subject, Subscription} from 'rxjs';
|
||||
|
||||
import {Event, NavigationEnd} from '../events';
|
||||
|
|
@ -17,7 +30,6 @@ import {ActivatedRoute} from '../router_state';
|
|||
import {Params} from '../shared';
|
||||
import {UrlTree} from '../url_tree';
|
||||
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
|
|
@ -124,7 +136,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* Represents an `href` attribute value applied to a host element,
|
||||
* when a host element is `<a>`. For other tags, the value is `null`.
|
||||
*/
|
||||
href: string|null = null;
|
||||
href: string | null = null;
|
||||
|
||||
/**
|
||||
* Represents the `target` attribute on a host element.
|
||||
|
|
@ -138,7 +150,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* @see {@link UrlCreationOptions#queryParams}
|
||||
* @see {@link Router#createUrlTree}
|
||||
*/
|
||||
@Input() queryParams?: Params|null;
|
||||
@Input() queryParams?: Params | null;
|
||||
/**
|
||||
* Passed to {@link Router#createUrlTree} as part of the
|
||||
* `UrlCreationOptions`.
|
||||
|
|
@ -152,7 +164,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* @see {@link UrlCreationOptions#queryParamsHandling}
|
||||
* @see {@link Router#createUrlTree}
|
||||
*/
|
||||
@Input() queryParamsHandling?: QueryParamsHandling|null;
|
||||
@Input() queryParamsHandling?: QueryParamsHandling | null;
|
||||
/**
|
||||
* Passed to {@link Router#navigateByUrl} as part of the
|
||||
* `NavigationBehaviorOptions`.
|
||||
|
|
@ -176,9 +188,9 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* @see {@link UrlCreationOptions#relativeTo}
|
||||
* @see {@link Router#createUrlTree}
|
||||
*/
|
||||
@Input() relativeTo?: ActivatedRoute|null;
|
||||
@Input() relativeTo?: ActivatedRoute | null;
|
||||
|
||||
private commands: any[]|null = null;
|
||||
private commands: any[] | null = null;
|
||||
|
||||
/** Whether a host element is an `<a>` tag. */
|
||||
private isAnchorElement: boolean;
|
||||
|
|
@ -189,10 +201,13 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
onChanges = new Subject<RouterLink>();
|
||||
|
||||
constructor(
|
||||
private router: Router, private route: ActivatedRoute,
|
||||
@Attribute('tabindex') private readonly tabIndexAttribute: string|null|undefined,
|
||||
private readonly renderer: Renderer2, private readonly el: ElementRef,
|
||||
private locationStrategy?: LocationStrategy) {
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@Attribute('tabindex') private readonly tabIndexAttribute: string | null | undefined,
|
||||
private readonly renderer: Renderer2,
|
||||
private readonly el: ElementRef,
|
||||
private locationStrategy?: LocationStrategy,
|
||||
) {
|
||||
const tagName = el.nativeElement.tagName?.toLowerCase();
|
||||
this.isAnchorElement = tagName === 'a' || tagName === 'area';
|
||||
|
||||
|
|
@ -235,7 +250,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* Modifies the tab index if there was not a tabindex attribute on the element during
|
||||
* instantiation.
|
||||
*/
|
||||
private setTabIndexIfNotOnNativeEl(newTabIndex: string|null) {
|
||||
private setTabIndexIfNotOnNativeEl(newTabIndex: string | null) {
|
||||
if (this.tabIndexAttribute != null /* both `null` and `undefined` */ || this.isAnchorElement) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -260,7 +275,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
* @see {@link Router#createUrlTree}
|
||||
*/
|
||||
@Input()
|
||||
set routerLink(commands: any[]|string|null|undefined) {
|
||||
set routerLink(commands: any[] | string | null | undefined) {
|
||||
if (commands != null) {
|
||||
this.commands = Array.isArray(commands) ? commands : [commands];
|
||||
this.setTabIndexIfNotOnNativeEl('0');
|
||||
|
|
@ -271,13 +286,22 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
/** @nodoc */
|
||||
@HostListener(
|
||||
'click',
|
||||
['$event.button', '$event.ctrlKey', '$event.shiftKey', '$event.altKey', '$event.metaKey'])
|
||||
onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean):
|
||||
boolean {
|
||||
@HostListener('click', [
|
||||
'$event.button',
|
||||
'$event.ctrlKey',
|
||||
'$event.shiftKey',
|
||||
'$event.altKey',
|
||||
'$event.metaKey',
|
||||
])
|
||||
onClick(
|
||||
button: number,
|
||||
ctrlKey: boolean,
|
||||
shiftKey: boolean,
|
||||
altKey: boolean,
|
||||
metaKey: boolean,
|
||||
): boolean {
|
||||
const urlTree = this.urlTree;
|
||||
|
||||
|
||||
if (urlTree === null) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -313,27 +337,33 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
|
||||
private updateHref(): void {
|
||||
const urlTree = this.urlTree;
|
||||
this.href = urlTree !== null && this.locationStrategy ?
|
||||
this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree)) :
|
||||
null;
|
||||
this.href =
|
||||
urlTree !== null && this.locationStrategy
|
||||
? this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree))
|
||||
: null;
|
||||
|
||||
const sanitizedValue = this.href === null ?
|
||||
null :
|
||||
// This class represents a directive that can be added to both `<a>` elements,
|
||||
// as well as other elements. As a result, we can't define security context at
|
||||
// compile time. So the security context is deferred to runtime.
|
||||
// The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function
|
||||
// based on the tag and property names. The logic mimics the one from
|
||||
// `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time.
|
||||
//
|
||||
// Note: we should investigate whether we can switch to using `@HostBinding('attr.href')`
|
||||
// instead of applying a value via a renderer, after a final merge of the
|
||||
// `RouterLinkWithHref` directive.
|
||||
ɵɵsanitizeUrlOrResourceUrl(this.href, this.el.nativeElement.tagName.toLowerCase(), 'href');
|
||||
const sanitizedValue =
|
||||
this.href === null
|
||||
? null
|
||||
: // This class represents a directive that can be added to both `<a>` elements,
|
||||
// as well as other elements. As a result, we can't define security context at
|
||||
// compile time. So the security context is deferred to runtime.
|
||||
// The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function
|
||||
// based on the tag and property names. The logic mimics the one from
|
||||
// `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time.
|
||||
//
|
||||
// Note: we should investigate whether we can switch to using `@HostBinding('attr.href')`
|
||||
// instead of applying a value via a renderer, after a final merge of the
|
||||
// `RouterLinkWithHref` directive.
|
||||
ɵɵsanitizeUrlOrResourceUrl(
|
||||
this.href,
|
||||
this.el.nativeElement.tagName.toLowerCase(),
|
||||
'href',
|
||||
);
|
||||
this.applyAttributeValue('href', sanitizedValue);
|
||||
}
|
||||
|
||||
private applyAttributeValue(attrName: string, attrValue: string|null) {
|
||||
private applyAttributeValue(attrName: string, attrValue: string | null) {
|
||||
const renderer = this.renderer;
|
||||
const nativeElement = this.el.nativeElement;
|
||||
if (attrValue !== null) {
|
||||
|
|
@ -343,7 +373,7 @@ export class RouterLink implements OnChanges, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
get urlTree(): UrlTree|null {
|
||||
get urlTree(): UrlTree | null {
|
||||
if (this.commands === null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,22 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Optional, Output, QueryList, Renderer2, SimpleChanges} from '@angular/core';
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectorRef,
|
||||
ContentChildren,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import {from, of, Subscription} from 'rxjs';
|
||||
import {mergeAll} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -16,7 +31,6 @@ import {IsActiveMatchOptions} from '../url_tree';
|
|||
|
||||
import {RouterLink} from './router_link';
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @description
|
||||
|
|
@ -110,8 +124,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
*
|
||||
* @see {@link Router#isActive}
|
||||
*/
|
||||
@Input() routerLinkActiveOptions: {exact: boolean}|IsActiveMatchOptions = {exact: false};
|
||||
|
||||
@Input() routerLinkActiveOptions: {exact: boolean} | IsActiveMatchOptions = {exact: false};
|
||||
|
||||
/**
|
||||
* Aria-current attribute to apply when the router link is active.
|
||||
|
|
@ -120,7 +133,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current}
|
||||
*/
|
||||
@Input() ariaCurrentWhenActive?: 'page'|'step'|'location'|'date'|'time'|true|false;
|
||||
@Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -141,8 +154,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
@Output() readonly isActiveChange: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
private router: Router, private element: ElementRef, private renderer: Renderer2,
|
||||
private readonly cdr: ChangeDetectorRef, @Optional() private link?: RouterLink) {
|
||||
private router: Router,
|
||||
private element: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
private readonly cdr: ChangeDetectorRef,
|
||||
@Optional() private link?: RouterLink,
|
||||
) {
|
||||
this.routerEventsSubscription = router.events.subscribe((s: Event) => {
|
||||
if (s instanceof NavigationEnd) {
|
||||
this.update();
|
||||
|
|
@ -153,28 +170,32 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
/** @nodoc */
|
||||
ngAfterContentInit(): void {
|
||||
// `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
|
||||
of(this.links.changes, of(null)).pipe(mergeAll()).subscribe(_ => {
|
||||
this.update();
|
||||
this.subscribeToEachLinkOnChanges();
|
||||
});
|
||||
of(this.links.changes, of(null))
|
||||
.pipe(mergeAll())
|
||||
.subscribe((_) => {
|
||||
this.update();
|
||||
this.subscribeToEachLinkOnChanges();
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeToEachLinkOnChanges() {
|
||||
this.linkInputChangesSubscription?.unsubscribe();
|
||||
const allLinkChanges = [...this.links.toArray(), this.link]
|
||||
.filter((link): link is RouterLink => !!link)
|
||||
.map(link => link.onChanges);
|
||||
this.linkInputChangesSubscription = from(allLinkChanges).pipe(mergeAll()).subscribe(link => {
|
||||
if (this._isActive !== this.isLinkActive(this.router)(link)) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
.filter((link): link is RouterLink => !!link)
|
||||
.map((link) => link.onChanges);
|
||||
this.linkInputChangesSubscription = from(allLinkChanges)
|
||||
.pipe(mergeAll())
|
||||
.subscribe((link) => {
|
||||
if (this._isActive !== this.isLinkActive(this.router)(link)) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Input()
|
||||
set routerLinkActive(data: string[]|string) {
|
||||
set routerLinkActive(data: string[] | string) {
|
||||
const classes = Array.isArray(data) ? data : data.split(' ');
|
||||
this.classes = classes.filter(c => !!c);
|
||||
this.classes = classes.filter((c) => !!c);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
|
|
@ -203,7 +224,10 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
});
|
||||
if (hasActiveLinks && this.ariaCurrentWhenActive !== undefined) {
|
||||
this.renderer.setAttribute(
|
||||
this.element.nativeElement, 'aria-current', this.ariaCurrentWhenActive.toString());
|
||||
this.element.nativeElement,
|
||||
'aria-current',
|
||||
this.ariaCurrentWhenActive.toString(),
|
||||
);
|
||||
} else {
|
||||
this.renderer.removeAttribute(this.element.nativeElement, 'aria-current');
|
||||
}
|
||||
|
|
@ -215,11 +239,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
}
|
||||
|
||||
private isLinkActive(router: Router): (link: RouterLink) => boolean {
|
||||
const options: boolean|IsActiveMatchOptions =
|
||||
isActiveMatchOptions(this.routerLinkActiveOptions) ?
|
||||
this.routerLinkActiveOptions :
|
||||
// While the types should disallow `undefined` here, it's possible without strict inputs
|
||||
(this.routerLinkActiveOptions.exact || false);
|
||||
const options: boolean | IsActiveMatchOptions = isActiveMatchOptions(
|
||||
this.routerLinkActiveOptions,
|
||||
)
|
||||
? this.routerLinkActiveOptions
|
||||
: // While the types should disallow `undefined` here, it's possible without strict inputs
|
||||
this.routerLinkActiveOptions.exact || false;
|
||||
return (link: RouterLink) => {
|
||||
const urlTree = link.urlTree;
|
||||
return urlTree ? router.isActive(urlTree, options) : false;
|
||||
|
|
@ -228,14 +253,15 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
|||
|
||||
private hasActiveLinks(): boolean {
|
||||
const isActiveCheckFn = this.isLinkActive(this.router);
|
||||
return this.link && isActiveCheckFn(this.link) || this.links.some(isActiveCheckFn);
|
||||
return (this.link && isActiveCheckFn(this.link)) || this.links.some(isActiveCheckFn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use instead of `'paths' in options` to be compatible with property renaming
|
||||
*/
|
||||
function isActiveMatchOptions(options: {exact: boolean}|
|
||||
IsActiveMatchOptions): options is IsActiveMatchOptions {
|
||||
function isActiveMatchOptions(
|
||||
options: {exact: boolean} | IsActiveMatchOptions,
|
||||
): options is IsActiveMatchOptions {
|
||||
return !!(options as IsActiveMatchOptions).paths;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,25 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, ComponentRef, Directive, EnvironmentInjector, EventEmitter, inject, Injectable, InjectionToken, Injector, Input, OnDestroy, OnInit, Output, reflectComponentType, SimpleChanges, ViewContainerRef, ɵRuntimeError as RuntimeError,} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
EnvironmentInjector,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Injectable,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
reflectComponentType,
|
||||
SimpleChanges,
|
||||
ViewContainerRef,
|
||||
ɵRuntimeError as RuntimeError,
|
||||
} from '@angular/core';
|
||||
import {combineLatest, of, Subscription} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -16,7 +34,6 @@ import {ChildrenOutletContexts} from '../router_outlet_context';
|
|||
import {ActivatedRoute} from '../router_state';
|
||||
import {PRIMARY_OUTLET} from '../shared';
|
||||
|
||||
|
||||
/**
|
||||
* An interface that defines the contract for developing a component outlet for the `Router`.
|
||||
*
|
||||
|
|
@ -39,7 +56,7 @@ export interface RouterOutletContract {
|
|||
isActivated: boolean;
|
||||
|
||||
/** The instance of the activated component or `null` if the outlet is not activated. */
|
||||
component: Object|null;
|
||||
component: Object | null;
|
||||
|
||||
/**
|
||||
* The `Data` of the `ActivatedRoute` snapshot.
|
||||
|
|
@ -49,12 +66,15 @@ export interface RouterOutletContract {
|
|||
/**
|
||||
* The `ActivatedRoute` for the outlet or `null` if the outlet is not activated.
|
||||
*/
|
||||
activatedRoute: ActivatedRoute|null;
|
||||
activatedRoute: ActivatedRoute | null;
|
||||
|
||||
/**
|
||||
* Called by the `Router` when the outlet should activate (create a component).
|
||||
*/
|
||||
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector|null): void;
|
||||
activateWith(
|
||||
activatedRoute: ActivatedRoute,
|
||||
environmentInjector: EnvironmentInjector | null,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* A request to destroy the currently activated component.
|
||||
|
|
@ -167,12 +187,12 @@ export interface RouterOutletContract {
|
|||
standalone: true,
|
||||
})
|
||||
export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
||||
private activated: ComponentRef<any>|null = null;
|
||||
private activated: ComponentRef<any> | null = null;
|
||||
/** @internal */
|
||||
get activatedComponentRef(): ComponentRef<any>|null {
|
||||
get activatedComponentRef(): ComponentRef<any> | null {
|
||||
return this.activated;
|
||||
}
|
||||
private _activatedRoute: ActivatedRoute|null = null;
|
||||
private _activatedRoute: ActivatedRoute | null = null;
|
||||
/**
|
||||
* The name of the outlet
|
||||
*
|
||||
|
|
@ -270,16 +290,18 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
|||
get component(): Object {
|
||||
if (!this.activated)
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
|
||||
);
|
||||
return this.activated.instance;
|
||||
}
|
||||
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated)
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
|
||||
);
|
||||
return this._activatedRoute as ActivatedRoute;
|
||||
}
|
||||
|
||||
|
|
@ -296,8 +318,9 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
|||
detach(): ComponentRef<any> {
|
||||
if (!this.activated)
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated');
|
||||
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
|
||||
);
|
||||
this.location.detach();
|
||||
const cmp = this.activated;
|
||||
this.activated = null;
|
||||
|
|
@ -327,12 +350,13 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
|||
}
|
||||
}
|
||||
|
||||
activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector|null) {
|
||||
activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector | null) {
|
||||
if (this.isActivated) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'Cannot activate an already activated outlet');
|
||||
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
'Cannot activate an already activated outlet',
|
||||
);
|
||||
}
|
||||
this._activatedRoute = activatedRoute;
|
||||
const location = this.location;
|
||||
|
|
@ -344,7 +368,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
|||
this.activated = location.createComponent(component, {
|
||||
index: location.length,
|
||||
injector,
|
||||
environmentInjector: environmentInjector ?? this.environmentInjector
|
||||
environmentInjector: environmentInjector ?? this.environmentInjector,
|
||||
});
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
|
|
@ -356,8 +380,10 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
|||
|
||||
class OutletInjector implements Injector {
|
||||
constructor(
|
||||
private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
|
||||
private parent: Injector) {}
|
||||
private route: ActivatedRoute,
|
||||
private childContexts: ChildrenOutletContexts,
|
||||
private parent: Injector,
|
||||
) {}
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === ActivatedRoute) {
|
||||
|
|
@ -390,7 +416,7 @@ export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
|
|||
*/
|
||||
@Injectable()
|
||||
export class RoutedComponentInputBinder {
|
||||
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>;
|
||||
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>();
|
||||
|
||||
bindActivatedRouteToOutletComponent(outlet: RouterOutlet) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
|
|
@ -404,43 +430,48 @@ export class RoutedComponentInputBinder {
|
|||
|
||||
private subscribeToRouteData(outlet: RouterOutlet) {
|
||||
const {activatedRoute} = outlet;
|
||||
const dataSubscription =
|
||||
combineLatest([
|
||||
activatedRoute.queryParams,
|
||||
activatedRoute.params,
|
||||
activatedRoute.data,
|
||||
])
|
||||
.pipe(switchMap(([queryParams, params, data], index) => {
|
||||
data = {...queryParams, ...params, ...data};
|
||||
// Get the first result from the data subscription synchronously so it's available to
|
||||
// the component as soon as possible (and doesn't require a second change detection).
|
||||
if (index === 0) {
|
||||
return of(data);
|
||||
}
|
||||
// Promise.resolve is used to avoid synchronously writing the wrong data when
|
||||
// two of the Observables in the `combineLatest` stream emit one after
|
||||
// another.
|
||||
return Promise.resolve(data);
|
||||
}))
|
||||
.subscribe(data => {
|
||||
// Outlet may have been deactivated or changed names to be associated with a different
|
||||
// route
|
||||
if (!outlet.isActivated || !outlet.activatedComponentRef ||
|
||||
outlet.activatedRoute !== activatedRoute || activatedRoute.component === null) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
const dataSubscription = combineLatest([
|
||||
activatedRoute.queryParams,
|
||||
activatedRoute.params,
|
||||
activatedRoute.data,
|
||||
])
|
||||
.pipe(
|
||||
switchMap(([queryParams, params, data], index) => {
|
||||
data = {...queryParams, ...params, ...data};
|
||||
// Get the first result from the data subscription synchronously so it's available to
|
||||
// the component as soon as possible (and doesn't require a second change detection).
|
||||
if (index === 0) {
|
||||
return of(data);
|
||||
}
|
||||
// Promise.resolve is used to avoid synchronously writing the wrong data when
|
||||
// two of the Observables in the `combineLatest` stream emit one after
|
||||
// another.
|
||||
return Promise.resolve(data);
|
||||
}),
|
||||
)
|
||||
.subscribe((data) => {
|
||||
// Outlet may have been deactivated or changed names to be associated with a different
|
||||
// route
|
||||
if (
|
||||
!outlet.isActivated ||
|
||||
!outlet.activatedComponentRef ||
|
||||
outlet.activatedRoute !== activatedRoute ||
|
||||
activatedRoute.component === null
|
||||
) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
const mirror = reflectComponentType(activatedRoute.component);
|
||||
if (!mirror) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
const mirror = reflectComponentType(activatedRoute.component);
|
||||
if (!mirror) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const {templateName} of mirror.inputs) {
|
||||
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
||||
}
|
||||
});
|
||||
for (const {templateName} of mirror.inputs) {
|
||||
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
||||
}
|
||||
});
|
||||
|
||||
this.outletDataSubscriptions.set(outlet, dataSubscription);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {UrlTree} from './url_tree';
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export type NavigationTrigger = 'imperative'|'popstate'|'hashchange';
|
||||
export type NavigationTrigger = 'imperative' | 'popstate' | 'hashchange';
|
||||
export const IMPERATIVE_NAVIGATION = 'imperative';
|
||||
|
||||
/**
|
||||
|
|
@ -73,10 +73,11 @@ export enum EventType {
|
|||
*/
|
||||
export class RouterEvent {
|
||||
constructor(
|
||||
/** A unique ID that the router assigns to every router navigation. */
|
||||
public id: number,
|
||||
/** The URL that is the destination for this navigation. */
|
||||
public url: string) {}
|
||||
/** A unique ID that the router assigns to every router navigation. */
|
||||
public id: number,
|
||||
/** The URL that is the destination for this navigation. */
|
||||
public url: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,17 +115,18 @@ export class NavigationStart extends RouterEvent {
|
|||
* remembered state, such as scroll position.
|
||||
*
|
||||
*/
|
||||
restoredState?: {[k: string]: any, navigationId: number}|null;
|
||||
restoredState?: {[k: string]: any; navigationId: number} | null;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
navigationTrigger: NavigationTrigger = 'imperative',
|
||||
/** @docsNotRequired */
|
||||
restoredState: {[k: string]: any, navigationId: number}|null = null) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
navigationTrigger: NavigationTrigger = 'imperative',
|
||||
/** @docsNotRequired */
|
||||
restoredState: {[k: string]: any; navigationId: number} | null = null,
|
||||
) {
|
||||
super(id, url);
|
||||
this.navigationTrigger = navigationTrigger;
|
||||
this.restoredState = restoredState;
|
||||
|
|
@ -149,19 +151,19 @@ export class NavigationEnd extends RouterEvent {
|
|||
readonly type = EventType.NavigationEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
/** @docsNotRequired */
|
||||
override toString(): string {
|
||||
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}')`;
|
||||
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,21 +227,22 @@ export class NavigationCancel extends RouterEvent {
|
|||
readonly type = EventType.NavigationCancel;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/**
|
||||
* A description of why the navigation was cancelled. For debug purposes only. Use `code`
|
||||
* instead for a stable cancellation reason that can be used in production.
|
||||
*/
|
||||
public reason: string,
|
||||
/**
|
||||
* A code to indicate why the navigation was canceled. This cancellation code is stable for
|
||||
* the reason and can be relied on whereas the `reason` string could change and should not be
|
||||
* used in production.
|
||||
*/
|
||||
readonly code?: NavigationCancellationCode) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/**
|
||||
* A description of why the navigation was cancelled. For debug purposes only. Use `code`
|
||||
* instead for a stable cancellation reason that can be used in production.
|
||||
*/
|
||||
public reason: string,
|
||||
/**
|
||||
* A code to indicate why the navigation was canceled. This cancellation code is stable for
|
||||
* the reason and can be relied on whereas the `reason` string could change and should not be
|
||||
* used in production.
|
||||
*/
|
||||
readonly code?: NavigationCancellationCode,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
|
|
@ -261,21 +264,22 @@ export class NavigationSkipped extends RouterEvent {
|
|||
readonly type = EventType.NavigationSkipped;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/**
|
||||
* A description of why the navigation was skipped. For debug purposes only. Use `code`
|
||||
* instead for a stable skipped reason that can be used in production.
|
||||
*/
|
||||
public reason: string,
|
||||
/**
|
||||
* A code to indicate why the navigation was skipped. This code is stable for
|
||||
* the reason and can be relied on whereas the `reason` string could change and should not be
|
||||
* used in production.
|
||||
*/
|
||||
readonly code?: NavigationSkippedCode) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/**
|
||||
* A description of why the navigation was skipped. For debug purposes only. Use `code`
|
||||
* instead for a stable skipped reason that can be used in production.
|
||||
*/
|
||||
public reason: string,
|
||||
/**
|
||||
* A code to indicate why the navigation was skipped. This code is stable for
|
||||
* the reason and can be relied on whereas the `reason` string could change and should not be
|
||||
* used in production.
|
||||
*/
|
||||
readonly code?: NavigationSkippedCode,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
}
|
||||
|
|
@ -293,19 +297,20 @@ export class NavigationError extends RouterEvent {
|
|||
readonly type = EventType.NavigationError;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public error: any,
|
||||
/**
|
||||
* The target of the navigation when the error occurred.
|
||||
*
|
||||
* Note that this can be `undefined` because an error could have occurred before the
|
||||
* `RouterStateSnapshot` was created for the navigation.
|
||||
*/
|
||||
readonly target?: RouterStateSnapshot) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public error: any,
|
||||
/**
|
||||
* The target of the navigation when the error occurred.
|
||||
*
|
||||
* Note that this can be `undefined` because an error could have occurred before the
|
||||
* `RouterStateSnapshot` was created for the navigation.
|
||||
*/
|
||||
readonly target?: RouterStateSnapshot,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
|
|
@ -324,21 +329,21 @@ export class RoutesRecognized extends RouterEvent {
|
|||
readonly type = EventType.RoutesRecognized;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
/** @docsNotRequired */
|
||||
override toString(): string {
|
||||
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}', state: ${this.state})`;
|
||||
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,20 +358,20 @@ export class GuardsCheckStart extends RouterEvent {
|
|||
readonly type = EventType.GuardsCheckStart;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}', state: ${this.state})`;
|
||||
return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -381,22 +386,22 @@ export class GuardsCheckEnd extends RouterEvent {
|
|||
readonly type = EventType.GuardsCheckEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
/** @docsNotRequired */
|
||||
public shouldActivate: boolean) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
/** @docsNotRequired */
|
||||
public shouldActivate: boolean,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`;
|
||||
return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -414,20 +419,20 @@ export class ResolveStart extends RouterEvent {
|
|||
readonly type = EventType.ResolveStart;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}', state: ${this.state})`;
|
||||
return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -441,20 +446,20 @@ export class ResolveEnd extends RouterEvent {
|
|||
readonly type = EventType.ResolveEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot) {
|
||||
/** @docsNotRequired */
|
||||
id: number,
|
||||
/** @docsNotRequired */
|
||||
url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot,
|
||||
) {
|
||||
super(id, url);
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${
|
||||
this.urlAfterRedirects}', state: ${this.state})`;
|
||||
return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -469,8 +474,9 @@ export class RouteConfigLoadStart {
|
|||
readonly type = EventType.RouteConfigLoadStart;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public route: Route) {}
|
||||
/** @docsNotRequired */
|
||||
public route: Route,
|
||||
) {}
|
||||
toString(): string {
|
||||
return `RouteConfigLoadStart(path: ${this.route.path})`;
|
||||
}
|
||||
|
|
@ -487,8 +493,9 @@ export class RouteConfigLoadEnd {
|
|||
readonly type = EventType.RouteConfigLoadEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public route: Route) {}
|
||||
/** @docsNotRequired */
|
||||
public route: Route,
|
||||
) {}
|
||||
toString(): string {
|
||||
return `RouteConfigLoadEnd(path: ${this.route.path})`;
|
||||
}
|
||||
|
|
@ -506,10 +513,11 @@ export class ChildActivationStart {
|
|||
readonly type = EventType.ChildActivationStart;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot) {}
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot,
|
||||
) {}
|
||||
toString(): string {
|
||||
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
|
||||
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
|
||||
return `ChildActivationStart(path: '${path}')`;
|
||||
}
|
||||
}
|
||||
|
|
@ -525,10 +533,11 @@ export class ChildActivationEnd {
|
|||
readonly type = EventType.ChildActivationEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot) {}
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot,
|
||||
) {}
|
||||
toString(): string {
|
||||
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
|
||||
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
|
||||
return `ChildActivationEnd(path: '${path}')`;
|
||||
}
|
||||
}
|
||||
|
|
@ -545,10 +554,11 @@ export class ActivationStart {
|
|||
readonly type = EventType.ActivationStart;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot) {}
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot,
|
||||
) {}
|
||||
toString(): string {
|
||||
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
|
||||
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
|
||||
return `ActivationStart(path: '${path}')`;
|
||||
}
|
||||
}
|
||||
|
|
@ -565,10 +575,11 @@ export class ActivationEnd {
|
|||
readonly type = EventType.ActivationEnd;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot) {}
|
||||
/** @docsNotRequired */
|
||||
public snapshot: ActivatedRouteSnapshot,
|
||||
) {}
|
||||
toString(): string {
|
||||
const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || '';
|
||||
const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || '';
|
||||
return `ActivationEnd(path: '${path}')`;
|
||||
}
|
||||
}
|
||||
|
|
@ -582,14 +593,15 @@ export class Scroll {
|
|||
readonly type = EventType.Scroll;
|
||||
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
readonly routerEvent: NavigationEnd|NavigationSkipped,
|
||||
/** @docsNotRequired */
|
||||
readonly routerEvent: NavigationEnd | NavigationSkipped,
|
||||
|
||||
/** @docsNotRequired */
|
||||
readonly position: [number, number]|null,
|
||||
/** @docsNotRequired */
|
||||
readonly position: [number, number] | null,
|
||||
|
||||
/** @docsNotRequired */
|
||||
readonly anchor: string|null) {}
|
||||
/** @docsNotRequired */
|
||||
readonly anchor: string | null,
|
||||
) {}
|
||||
|
||||
toString(): string {
|
||||
const pos = this.position ? `${this.position[0]}, ${this.position[1]}` : null;
|
||||
|
|
@ -601,7 +613,7 @@ export class BeforeActivateRoutes {}
|
|||
export class RedirectRequest {
|
||||
constructor(readonly url: UrlTree) {}
|
||||
}
|
||||
export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest;
|
||||
export type PrivateRouterEvents = BeforeActivateRoutes | RedirectRequest;
|
||||
|
||||
/**
|
||||
* Router events that allow you to track the lifecycle of the router.
|
||||
|
|
@ -636,10 +648,24 @@ export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest;
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export type Event = NavigationStart|NavigationEnd|NavigationCancel|NavigationError|RoutesRecognized|
|
||||
GuardsCheckStart|GuardsCheckEnd|RouteConfigLoadStart|RouteConfigLoadEnd|ChildActivationStart|
|
||||
ChildActivationEnd|ActivationStart|ActivationEnd|Scroll|ResolveStart|ResolveEnd|
|
||||
NavigationSkipped;
|
||||
export type Event =
|
||||
| NavigationStart
|
||||
| NavigationEnd
|
||||
| NavigationCancel
|
||||
| NavigationError
|
||||
| RoutesRecognized
|
||||
| GuardsCheckStart
|
||||
| GuardsCheckEnd
|
||||
| RouteConfigLoadStart
|
||||
| RouteConfigLoadEnd
|
||||
| ChildActivationStart
|
||||
| ChildActivationEnd
|
||||
| ActivationStart
|
||||
| ActivationEnd
|
||||
| Scroll
|
||||
| ResolveStart
|
||||
| ResolveEnd
|
||||
| NavigationSkipped;
|
||||
|
||||
export function stringifyEvent(routerEvent: Event): string {
|
||||
switch (routerEvent.type) {
|
||||
|
|
@ -652,42 +678,33 @@ export function stringifyEvent(routerEvent: Event): string {
|
|||
case EventType.ChildActivationStart:
|
||||
return `ChildActivationStart(path: '${routerEvent.snapshot.routeConfig?.path || ''}')`;
|
||||
case EventType.GuardsCheckEnd:
|
||||
return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${
|
||||
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
|
||||
routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`;
|
||||
return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`;
|
||||
case EventType.GuardsCheckStart:
|
||||
return `GuardsCheckStart(id: ${routerEvent.id}, url: '${
|
||||
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
|
||||
routerEvent.state})`;
|
||||
return `GuardsCheckStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
case EventType.NavigationCancel:
|
||||
return `NavigationCancel(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
|
||||
case EventType.NavigationSkipped:
|
||||
return `NavigationSkipped(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
|
||||
case EventType.NavigationEnd:
|
||||
return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
|
||||
routerEvent.urlAfterRedirects}')`;
|
||||
return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}')`;
|
||||
case EventType.NavigationError:
|
||||
return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${
|
||||
routerEvent.error})`;
|
||||
return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${routerEvent.error})`;
|
||||
case EventType.NavigationStart:
|
||||
return `NavigationStart(id: ${routerEvent.id}, url: '${routerEvent.url}')`;
|
||||
case EventType.ResolveEnd:
|
||||
return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
|
||||
routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
case EventType.ResolveStart:
|
||||
return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${
|
||||
routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
case EventType.RouteConfigLoadEnd:
|
||||
return `RouteConfigLoadEnd(path: ${routerEvent.route.path})`;
|
||||
case EventType.RouteConfigLoadStart:
|
||||
return `RouteConfigLoadStart(path: ${routerEvent.route.path})`;
|
||||
case EventType.RoutesRecognized:
|
||||
return `RoutesRecognized(id: ${routerEvent.id}, url: '${
|
||||
routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${
|
||||
routerEvent.state})`;
|
||||
return `RoutesRecognized(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`;
|
||||
case EventType.Scroll:
|
||||
const pos =
|
||||
routerEvent.position ? `${routerEvent.position[0]}, ${routerEvent.position[1]}` : null;
|
||||
const pos = routerEvent.position
|
||||
? `${routerEvent.position[0]}, ${routerEvent.position[1]}`
|
||||
: null;
|
||||
return `Scroll(anchor: '${routerEvent.anchor}', position: '${pos}')`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,31 +6,131 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
export {createUrlTreeFromSnapshot} from './create_url_tree';
|
||||
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
export {RouterLinkActive} from './directives/router_link_active';
|
||||
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
|
||||
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
|
||||
export {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
|
||||
export {
|
||||
ActivationEnd,
|
||||
ActivationStart,
|
||||
ChildActivationEnd,
|
||||
ChildActivationStart,
|
||||
Event,
|
||||
EventType,
|
||||
GuardsCheckEnd,
|
||||
GuardsCheckStart,
|
||||
NavigationCancel,
|
||||
NavigationCancellationCode as NavigationCancellationCode,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationSkipped,
|
||||
NavigationSkippedCode,
|
||||
NavigationStart,
|
||||
ResolveEnd,
|
||||
ResolveStart,
|
||||
RouteConfigLoadEnd,
|
||||
RouteConfigLoadStart,
|
||||
RouterEvent,
|
||||
RoutesRecognized,
|
||||
Scroll,
|
||||
} from './events';
|
||||
export {
|
||||
CanActivateChildFn,
|
||||
CanActivateFn,
|
||||
CanDeactivateFn,
|
||||
CanLoadFn,
|
||||
CanMatchFn,
|
||||
Data,
|
||||
DefaultExport,
|
||||
LoadChildren,
|
||||
LoadChildrenCallback,
|
||||
NavigationBehaviorOptions,
|
||||
OnSameUrlNavigation,
|
||||
QueryParamsHandling,
|
||||
ResolveData,
|
||||
ResolveFn,
|
||||
Route,
|
||||
Routes,
|
||||
RunGuardsAndResolvers,
|
||||
UrlMatcher,
|
||||
UrlMatchResult,
|
||||
} from './models';
|
||||
export {ViewTransitionInfo, ViewTransitionsFeatureOptions} from './utils/view_transition';
|
||||
|
||||
export * from './models_deprecated';
|
||||
export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition';
|
||||
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
|
||||
export {DebugTracingFeature, DisabledInitialNavigationFeature, withViewTransitions, ViewTransitionsFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, NavigationErrorHandlerFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withComponentInputBinding, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withNavigationErrorHandler, withPreloading, withRouterConfig} from './provide_router';
|
||||
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
export {
|
||||
DebugTracingFeature,
|
||||
DisabledInitialNavigationFeature,
|
||||
withViewTransitions,
|
||||
ViewTransitionsFeature,
|
||||
EnabledBlockingInitialNavigationFeature,
|
||||
InitialNavigationFeature,
|
||||
InMemoryScrollingFeature,
|
||||
NavigationErrorHandlerFeature,
|
||||
PreloadingFeature,
|
||||
provideRouter,
|
||||
provideRoutes,
|
||||
RouterConfigurationFeature,
|
||||
RouterFeature,
|
||||
RouterFeatures,
|
||||
RouterHashLocationFeature,
|
||||
withComponentInputBinding,
|
||||
withDebugTracing,
|
||||
withDisabledInitialNavigation,
|
||||
withEnabledBlockingInitialNavigation,
|
||||
withHashLocation,
|
||||
withInMemoryScrolling,
|
||||
withNavigationErrorHandler,
|
||||
withPreloading,
|
||||
withRouterConfig,
|
||||
} from './provide_router';
|
||||
export {
|
||||
BaseRouteReuseStrategy,
|
||||
DetachedRouteHandle,
|
||||
RouteReuseStrategy,
|
||||
} from './route_reuse_strategy';
|
||||
export {Router} from './router';
|
||||
export {ExtraOptions, InitialNavigation, InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
|
||||
export {
|
||||
ExtraOptions,
|
||||
InitialNavigation,
|
||||
InMemoryScrollingOptions,
|
||||
ROUTER_CONFIGURATION,
|
||||
RouterConfigOptions,
|
||||
} from './router_config';
|
||||
export {ROUTES} from './router_config_loader';
|
||||
export {ROUTER_INITIALIZER, RouterModule} from './router_module';
|
||||
export {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
|
||||
export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
export {
|
||||
NoPreloading,
|
||||
PreloadAllModules,
|
||||
PreloadingStrategy,
|
||||
RouterPreloader,
|
||||
} from './router_preloader';
|
||||
export {
|
||||
ActivatedRoute,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterState,
|
||||
RouterStateSnapshot,
|
||||
} from './router_state';
|
||||
export {convertToParamMap, defaultUrlMatcher, ParamMap, Params, PRIMARY_OUTLET} from './shared';
|
||||
export {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
export {DefaultUrlSerializer, IsActiveMatchOptions, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
export {mapToCanActivate, mapToCanActivateChild, mapToCanDeactivate, mapToCanMatch, mapToResolve} from './utils/functional_guards';
|
||||
export {
|
||||
DefaultUrlSerializer,
|
||||
IsActiveMatchOptions,
|
||||
UrlSegment,
|
||||
UrlSegmentGroup,
|
||||
UrlSerializer,
|
||||
UrlTree,
|
||||
} from './url_tree';
|
||||
export {
|
||||
mapToCanActivate,
|
||||
mapToCanActivateChild,
|
||||
mapToCanDeactivate,
|
||||
mapToCanMatch,
|
||||
mapToResolve,
|
||||
} from './utils/functional_guards';
|
||||
export {VERSION} from './version';
|
||||
|
||||
export * from './private_export';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {EnvironmentInjector, EnvironmentProviders, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core';
|
||||
import {
|
||||
EnvironmentInjector,
|
||||
EnvironmentProviders,
|
||||
NgModuleFactory,
|
||||
Provider,
|
||||
ProviderToken,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||
|
|
@ -36,7 +43,7 @@ import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
|
|||
* @see {@link NavigationBehaviorOptions}
|
||||
* @see {@link RouterConfigOptions}
|
||||
*/
|
||||
export type OnSameUrlNavigation = 'reload'|'ignore';
|
||||
export type OnSameUrlNavigation = 'reload' | 'ignore';
|
||||
|
||||
/**
|
||||
* The `InjectionToken` and `@Injectable` classes for guards and resolvers are deprecated in favor
|
||||
|
|
@ -55,7 +62,7 @@ export type OnSameUrlNavigation = 'reload'|'ignore';
|
|||
* @see {@link core/inject}
|
||||
* @publicApi
|
||||
*/
|
||||
export type DeprecatedGuard = ProviderToken<any>|any;
|
||||
export type DeprecatedGuard = ProviderToken<any> | any;
|
||||
|
||||
/**
|
||||
* Represents a route configuration for the Router service.
|
||||
|
|
@ -105,8 +112,11 @@ export type UrlMatchResult = {
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) =>
|
||||
UrlMatchResult|null;
|
||||
export type UrlMatcher = (
|
||||
segments: UrlSegment[],
|
||||
group: UrlSegmentGroup,
|
||||
route: Route,
|
||||
) => UrlMatchResult | null;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -117,7 +127,7 @@ export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route:
|
|||
* @publicApi
|
||||
*/
|
||||
export type Data = {
|
||||
[key: string|symbol]: any
|
||||
[key: string | symbol]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -129,7 +139,7 @@ export type Data = {
|
|||
* @publicApi
|
||||
*/
|
||||
export type ResolveData = {
|
||||
[key: string|symbol]: ResolveFn<unknown>|DeprecatedGuard
|
||||
[key: string | symbol]: ResolveFn<unknown> | DeprecatedGuard;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -183,9 +193,14 @@ export interface DefaultExport<T> {
|
|||
* @see {@link Route#loadChildren}
|
||||
* @publicApi
|
||||
*/
|
||||
export type LoadChildrenCallback = () => Type<any>|NgModuleFactory<any>|Routes|
|
||||
Observable<Type<any>|Routes|DefaultExport<Type<any>>|DefaultExport<Routes>>|
|
||||
Promise<NgModuleFactory<any>|Type<any>|Routes|DefaultExport<Type<any>>|DefaultExport<Routes>>;
|
||||
export type LoadChildrenCallback = () =>
|
||||
| Type<any>
|
||||
| NgModuleFactory<any>
|
||||
| Routes
|
||||
| Observable<Type<any> | Routes | DefaultExport<Type<any>> | DefaultExport<Routes>>
|
||||
| Promise<
|
||||
NgModuleFactory<any> | Type<any> | Routes | DefaultExport<Type<any>> | DefaultExport<Routes>
|
||||
>;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -208,7 +223,7 @@ export type LoadChildren = LoadChildrenCallback;
|
|||
* @see {@link RouterLink}
|
||||
* @publicApi
|
||||
*/
|
||||
export type QueryParamsHandling = 'merge'|'preserve'|'';
|
||||
export type QueryParamsHandling = 'merge' | 'preserve' | '';
|
||||
|
||||
/**
|
||||
* A policy for when to run guards and resolvers on a route.
|
||||
|
|
@ -229,8 +244,12 @@ export type QueryParamsHandling = 'merge'|'preserve'|'';
|
|||
* @publicApi
|
||||
*/
|
||||
export type RunGuardsAndResolvers =
|
||||
'pathParamsChange'|'pathParamsOrQueryParamsChange'|'paramsChange'|'paramsOrQueryParamsChange'|
|
||||
'always'|((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);
|
||||
| 'pathParamsChange'
|
||||
| 'pathParamsOrQueryParamsChange'
|
||||
| 'paramsChange'
|
||||
| 'paramsOrQueryParamsChange'
|
||||
| 'always'
|
||||
| ((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);
|
||||
|
||||
/**
|
||||
* A configuration object that defines a single route.
|
||||
|
|
@ -464,7 +483,7 @@ export interface Route {
|
|||
*
|
||||
* @see {@link TitleStrategy}
|
||||
*/
|
||||
title?: string|Type<Resolve<string>>|ResolveFn<string>;
|
||||
title?: string | Type<Resolve<string>> | ResolveFn<string>;
|
||||
|
||||
/**
|
||||
* The path to match against. Cannot be used together with a custom `matcher` function.
|
||||
|
|
@ -493,7 +512,7 @@ export interface Route {
|
|||
* to the redirect destination, creating an endless loop.
|
||||
*
|
||||
*/
|
||||
pathMatch?: 'prefix'|'full';
|
||||
pathMatch?: 'prefix' | 'full';
|
||||
/**
|
||||
* A custom URL-matching function. Cannot be used together with `path`.
|
||||
*/
|
||||
|
|
@ -507,8 +526,10 @@ export interface Route {
|
|||
/**
|
||||
* An object specifying a lazy-loaded component.
|
||||
*/
|
||||
loadComponent?: () => Type<unknown>| Observable<Type<unknown>|DefaultExport<Type<unknown>>>|
|
||||
Promise<Type<unknown>|DefaultExport<Type<unknown>>>;
|
||||
loadComponent?: () =>
|
||||
| Type<unknown>
|
||||
| Observable<Type<unknown> | DefaultExport<Type<unknown>>>
|
||||
| Promise<Type<unknown> | DefaultExport<Type<unknown>>>;
|
||||
/**
|
||||
* Filled for routes `loadComponent` once the component is loaded.
|
||||
* @internal
|
||||
|
|
@ -536,7 +557,7 @@ export interface Route {
|
|||
* When using a function rather than DI tokens, the function can call `inject` to get any required
|
||||
* dependencies. This `inject` call must be done in a synchronous context.
|
||||
*/
|
||||
canActivate?: Array<CanActivateFn|DeprecatedGuard>;
|
||||
canActivate?: Array<CanActivateFn | DeprecatedGuard>;
|
||||
/**
|
||||
* An array of `CanMatchFn` or DI tokens used to look up `CanMatch()`
|
||||
* handlers, in order to determine if the current user is allowed to
|
||||
|
|
@ -545,7 +566,7 @@ export interface Route {
|
|||
* When using a function rather than DI tokens, the function can call `inject` to get any required
|
||||
* dependencies. This `inject` call must be done in a synchronous context.
|
||||
*/
|
||||
canMatch?: Array<CanMatchFn|DeprecatedGuard>;
|
||||
canMatch?: Array<CanMatchFn | DeprecatedGuard>;
|
||||
/**
|
||||
* An array of `CanActivateChildFn` or DI tokens used to look up `CanActivateChild()` handlers,
|
||||
* in order to determine if the current user is allowed to activate
|
||||
|
|
@ -554,7 +575,7 @@ export interface Route {
|
|||
* When using a function rather than DI tokens, the function can call `inject` to get any required
|
||||
* dependencies. This `inject` call must be done in a synchronous context.
|
||||
*/
|
||||
canActivateChild?: Array<CanActivateChildFn|DeprecatedGuard>;
|
||||
canActivateChild?: Array<CanActivateChildFn | DeprecatedGuard>;
|
||||
/**
|
||||
* An array of `CanDeactivateFn` or DI tokens used to look up `CanDeactivate()`
|
||||
* handlers, in order to determine if the current user is allowed to
|
||||
|
|
@ -563,7 +584,7 @@ export interface Route {
|
|||
* When using a function rather than DI tokens, the function can call `inject` to get any required
|
||||
* dependencies. This `inject` call must be done in a synchronous context.
|
||||
*/
|
||||
canDeactivate?: Array<CanDeactivateFn<any>|DeprecatedGuard>;
|
||||
canDeactivate?: Array<CanDeactivateFn<any> | DeprecatedGuard>;
|
||||
/**
|
||||
* An array of `CanLoadFn` or DI tokens used to look up `CanLoad()`
|
||||
* handlers, in order to determine if the current user is allowed to
|
||||
|
|
@ -573,7 +594,7 @@ export interface Route {
|
|||
* dependencies. This `inject` call must be done in a synchronous context.
|
||||
* @deprecated Use `canMatch` instead
|
||||
*/
|
||||
canLoad?: Array<CanLoadFn|DeprecatedGuard>;
|
||||
canLoad?: Array<CanLoadFn | DeprecatedGuard>;
|
||||
/**
|
||||
* Additional developer-defined data provided to the component via
|
||||
* `ActivatedRoute`. By default, no additional data is passed.
|
||||
|
|
@ -620,7 +641,7 @@ export interface Route {
|
|||
* route also has a `loadChildren` function which returns an `NgModuleRef`, this injector will be
|
||||
* used as the parent of the lazy loaded module.
|
||||
*/
|
||||
providers?: Array<Provider|EnvironmentProviders>;
|
||||
providers?: Array<Provider | EnvironmentProviders>;
|
||||
|
||||
/**
|
||||
* Injector created from the static route providers
|
||||
|
|
@ -643,7 +664,7 @@ export interface Route {
|
|||
|
||||
export interface LoadedRouterConfig {
|
||||
routes: Route[];
|
||||
injector: EnvironmentInjector|undefined;
|
||||
injector: EnvironmentInjector | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -704,8 +725,10 @@ export interface LoadedRouterConfig {
|
|||
* @see {@link CanActivateFn}
|
||||
*/
|
||||
export interface CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -728,8 +751,10 @@ export interface CanActivate {
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
export type CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
@ -794,8 +819,10 @@ export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSn
|
|||
* @see {@link CanActivateChildFn}
|
||||
*/
|
||||
export interface CanActivateChild {
|
||||
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot):
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
canActivateChild(
|
||||
childRoute: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -813,8 +840,10 @@ export interface CanActivateChild {
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
export type CanActivateChildFn = (
|
||||
childRoute: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
@ -878,9 +907,11 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou
|
|||
*/
|
||||
export interface CanDeactivate<T> {
|
||||
canDeactivate(
|
||||
component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean
|
||||
|UrlTree;
|
||||
component: T,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot,
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -898,10 +929,12 @@ export interface CanDeactivate<T> {
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export type CanDeactivateFn<T> =
|
||||
(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot) =>
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
export type CanDeactivateFn<T> = (
|
||||
component: T,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot,
|
||||
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
@ -969,8 +1002,10 @@ export type CanDeactivateFn<T> =
|
|||
* @see {@link CanMatchFn}
|
||||
*/
|
||||
export interface CanMatch {
|
||||
canMatch(route: Route, segments: UrlSegment[]):
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
canMatch(
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -988,8 +1023,10 @@ export interface CanMatch {
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export type CanMatchFn = (route: Route, segments: UrlSegment[]) =>
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
export type CanMatchFn = (
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
@ -1089,7 +1126,10 @@ export type CanMatchFn = (route: Route, segments: UrlSegment[]) =>
|
|||
* @see {@link ResolveFn}
|
||||
*/
|
||||
export interface Resolve<T> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T>|Promise<T>|T;
|
||||
resolve(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): Observable<T> | Promise<T> | T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1133,8 +1173,10 @@ export interface Resolve<T> {
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
|
||||
Observable<T>|Promise<T>|T;
|
||||
export type ResolveFn<T> = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => Observable<T> | Promise<T> | T;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
@ -1191,8 +1233,10 @@ export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSna
|
|||
* @deprecated Use {@link CanMatchFn} instead
|
||||
*/
|
||||
export interface CanLoad {
|
||||
canLoad(route: Route, segments: UrlSegment[]):
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
canLoad(
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1204,9 +1248,10 @@ export interface CanLoad {
|
|||
* @see {@link CanMatchFn}
|
||||
* @deprecated Use `Route.canMatch` and `CanMatchFn` instead
|
||||
*/
|
||||
export type CanLoadFn = (route: Route, segments: UrlSegment[]) =>
|
||||
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
|
||||
|
||||
export type CanLoadFn = (
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
|
||||
/**
|
||||
* @description
|
||||
|
|
|
|||
|
|
@ -10,4 +10,12 @@
|
|||
// The public API re-exports everything from this file, which can be patched
|
||||
// locally in g3 to prevent regressions after cleanups complete.
|
||||
|
||||
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, DeprecatedGuard, Resolve} from './models';
|
||||
export {
|
||||
CanActivate,
|
||||
CanActivateChild,
|
||||
CanDeactivate,
|
||||
CanLoad,
|
||||
CanMatch,
|
||||
DeprecatedGuard,
|
||||
Resolve,
|
||||
} from './models';
|
||||
|
|
|
|||
|
|
@ -12,28 +12,36 @@ import {isUrlTree, UrlSerializer, UrlTree} from './url_tree';
|
|||
|
||||
export const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';
|
||||
|
||||
export type NavigationCancelingError =
|
||||
Error&{[NAVIGATION_CANCELING_ERROR]: true, cancellationCode: NavigationCancellationCode};
|
||||
export type RedirectingNavigationCancelingError = NavigationCancelingError&{
|
||||
export type NavigationCancelingError = Error & {
|
||||
[NAVIGATION_CANCELING_ERROR]: true;
|
||||
cancellationCode: NavigationCancellationCode;
|
||||
};
|
||||
export type RedirectingNavigationCancelingError = NavigationCancelingError & {
|
||||
url: UrlTree;
|
||||
navigationBehaviorOptions?: NavigationBehaviorOptions;
|
||||
cancellationCode: NavigationCancellationCode.Redirect;
|
||||
};
|
||||
|
||||
export function redirectingNavigationError(
|
||||
urlSerializer: UrlSerializer, redirect: UrlTree): RedirectingNavigationCancelingError {
|
||||
const {redirectTo, navigationBehaviorOptions} =
|
||||
isUrlTree(redirect) ? {redirectTo: redirect, navigationBehaviorOptions: undefined} : redirect;
|
||||
urlSerializer: UrlSerializer,
|
||||
redirect: UrlTree,
|
||||
): RedirectingNavigationCancelingError {
|
||||
const {redirectTo, navigationBehaviorOptions} = isUrlTree(redirect)
|
||||
? {redirectTo: redirect, navigationBehaviorOptions: undefined}
|
||||
: redirect;
|
||||
const error = navigationCancelingError(
|
||||
ngDevMode && `Redirecting to "${urlSerializer.serialize(redirectTo)}"`,
|
||||
NavigationCancellationCode.Redirect) as RedirectingNavigationCancelingError;
|
||||
ngDevMode && `Redirecting to "${urlSerializer.serialize(redirectTo)}"`,
|
||||
NavigationCancellationCode.Redirect,
|
||||
) as RedirectingNavigationCancelingError;
|
||||
error.url = redirectTo;
|
||||
error.navigationBehaviorOptions = navigationBehaviorOptions;
|
||||
return error;
|
||||
}
|
||||
|
||||
export function navigationCancelingError(
|
||||
message: string|null|false, code: NavigationCancellationCode) {
|
||||
message: string | null | false,
|
||||
code: NavigationCancellationCode,
|
||||
) {
|
||||
const error = new Error(`NavigationCancelingError: ${message || ''}`) as NavigationCancelingError;
|
||||
error[NAVIGATION_CANCELING_ERROR] = true;
|
||||
error.cancellationCode = code;
|
||||
|
|
@ -41,10 +49,12 @@ export function navigationCancelingError(
|
|||
}
|
||||
|
||||
export function isRedirectingNavigationCancelingError(
|
||||
error: unknown|
|
||||
RedirectingNavigationCancelingError): error is RedirectingNavigationCancelingError {
|
||||
return isNavigationCancelingError(error) &&
|
||||
isUrlTree((error as RedirectingNavigationCancelingError).url);
|
||||
error: unknown | RedirectingNavigationCancelingError,
|
||||
): error is RedirectingNavigationCancelingError {
|
||||
return (
|
||||
isNavigationCancelingError(error) &&
|
||||
isUrlTree((error as RedirectingNavigationCancelingError).url)
|
||||
);
|
||||
}
|
||||
|
||||
export function isNavigationCancelingError(error: unknown): error is NavigationCancelingError {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -19,22 +19,31 @@ import {nodeChildrenAsMap, TreeNode} from '../utils/tree';
|
|||
|
||||
let warnedAboutUnsupportedInputBinding = false;
|
||||
|
||||
export const activateRoutes =
|
||||
(rootContexts: ChildrenOutletContexts, routeReuseStrategy: RouteReuseStrategy,
|
||||
forwardEvent: (evt: Event) => void,
|
||||
inputBindingEnabled: boolean): MonoTypeOperatorFunction<NavigationTransition> => map(t => {
|
||||
new ActivateRoutes(
|
||||
routeReuseStrategy, t.targetRouterState!, t.currentRouterState, forwardEvent,
|
||||
inputBindingEnabled)
|
||||
.activate(rootContexts);
|
||||
return t;
|
||||
});
|
||||
export const activateRoutes = (
|
||||
rootContexts: ChildrenOutletContexts,
|
||||
routeReuseStrategy: RouteReuseStrategy,
|
||||
forwardEvent: (evt: Event) => void,
|
||||
inputBindingEnabled: boolean,
|
||||
): MonoTypeOperatorFunction<NavigationTransition> =>
|
||||
map((t) => {
|
||||
new ActivateRoutes(
|
||||
routeReuseStrategy,
|
||||
t.targetRouterState!,
|
||||
t.currentRouterState,
|
||||
forwardEvent,
|
||||
inputBindingEnabled,
|
||||
).activate(rootContexts);
|
||||
return t;
|
||||
});
|
||||
|
||||
export class ActivateRoutes {
|
||||
constructor(
|
||||
private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState,
|
||||
private currState: RouterState, private forwardEvent: (evt: Event) => void,
|
||||
private inputBindingEnabled: boolean) {}
|
||||
private routeReuseStrategy: RouteReuseStrategy,
|
||||
private futureState: RouterState,
|
||||
private currState: RouterState,
|
||||
private forwardEvent: (evt: Event) => void,
|
||||
private inputBindingEnabled: boolean,
|
||||
) {}
|
||||
|
||||
activate(parentContexts: ChildrenOutletContexts): void {
|
||||
const futureRoot = this.futureState._root;
|
||||
|
|
@ -47,12 +56,14 @@ export class ActivateRoutes {
|
|||
|
||||
// De-activate the child route that are not re-used for the future state
|
||||
private deactivateChildRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
||||
contexts: ChildrenOutletContexts): void {
|
||||
futureNode: TreeNode<ActivatedRoute>,
|
||||
currNode: TreeNode<ActivatedRoute> | null,
|
||||
contexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
|
||||
|
||||
// Recurse on the routes active in the future state to de-activate deeper children
|
||||
futureNode.children.forEach(futureChild => {
|
||||
futureNode.children.forEach((futureChild) => {
|
||||
const childOutletName = futureChild.value.outlet;
|
||||
this.deactivateRoutes(futureChild, children[childOutletName], contexts);
|
||||
delete children[childOutletName];
|
||||
|
|
@ -65,8 +76,10 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private deactivateRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
|
||||
parentContext: ChildrenOutletContexts): void {
|
||||
futureNode: TreeNode<ActivatedRoute>,
|
||||
currNode: TreeNode<ActivatedRoute>,
|
||||
parentContext: ChildrenOutletContexts,
|
||||
): void {
|
||||
const future = futureNode.value;
|
||||
const curr = currNode ? currNode.value : null;
|
||||
|
||||
|
|
@ -91,7 +104,9 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private deactivateRouteAndItsChildren(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
|
||||
route: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
// If there is no component, the Route is never attached to an outlet (because there is no
|
||||
// component to attach).
|
||||
if (route.value.component && this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
|
||||
|
|
@ -102,7 +117,9 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private detachAndStoreRouteSubtree(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
|
||||
route: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
const context = parentContexts.getContext(route.value.outlet);
|
||||
const contexts = context && route.value.component ? context.children : parentContexts;
|
||||
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(route);
|
||||
|
|
@ -119,7 +136,9 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private deactivateRouteAndOutlet(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
|
||||
route: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
const context = parentContexts.getContext(route.value.outlet);
|
||||
// The context could be `null` if we are on a componentless route but there may still be
|
||||
// children that need deactivating.
|
||||
|
|
@ -146,10 +165,12 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private activateChildRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
||||
contexts: ChildrenOutletContexts): void {
|
||||
futureNode: TreeNode<ActivatedRoute>,
|
||||
currNode: TreeNode<ActivatedRoute> | null,
|
||||
contexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
const children: {[outlet: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
|
||||
futureNode.children.forEach(c => {
|
||||
futureNode.children.forEach((c) => {
|
||||
this.activateRoutes(c, children[c.value.outlet], contexts);
|
||||
this.forwardEvent(new ActivationEnd(c.value.snapshot));
|
||||
});
|
||||
|
|
@ -159,8 +180,10 @@ export class ActivateRoutes {
|
|||
}
|
||||
|
||||
private activateRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts): void {
|
||||
futureNode: TreeNode<ActivatedRoute>,
|
||||
currNode: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts,
|
||||
): void {
|
||||
const future = futureNode.value;
|
||||
const curr = currNode ? currNode.value : null;
|
||||
|
||||
|
|
@ -182,8 +205,9 @@ export class ActivateRoutes {
|
|||
const context = parentContexts.getOrCreateContext(future.outlet);
|
||||
|
||||
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
|
||||
const stored =
|
||||
(<DetachedRouteHandleInternal>this.routeReuseStrategy.retrieve(future.snapshot));
|
||||
const stored = <DetachedRouteHandleInternal>(
|
||||
this.routeReuseStrategy.retrieve(future.snapshot)
|
||||
);
|
||||
this.routeReuseStrategy.store(future.snapshot, null);
|
||||
context.children.onOutletReAttached(stored.contexts);
|
||||
context.attachRef = stored.componentRef;
|
||||
|
|
@ -214,14 +238,19 @@ export class ActivateRoutes {
|
|||
this.activateChildRoutes(futureNode, null, parentContexts);
|
||||
}
|
||||
}
|
||||
if ((typeof ngDevMode === 'undefined' || ngDevMode)) {
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
const context = parentContexts.getOrCreateContext(future.outlet);
|
||||
const outlet = context.outlet;
|
||||
if (outlet && this.inputBindingEnabled && !outlet.supportsBindingToComponentInputs &&
|
||||
!warnedAboutUnsupportedInputBinding) {
|
||||
if (
|
||||
outlet &&
|
||||
this.inputBindingEnabled &&
|
||||
!outlet.supportsBindingToComponentInputs &&
|
||||
!warnedAboutUnsupportedInputBinding
|
||||
) {
|
||||
console.warn(
|
||||
`'withComponentInputBinding' feature is enabled but ` +
|
||||
`this application is using an outlet that may not support binding to component inputs.`);
|
||||
`'withComponentInputBinding' feature is enabled but ` +
|
||||
`this application is using an outlet that may not support binding to component inputs.`,
|
||||
);
|
||||
warnedAboutUnsupportedInputBinding = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,66 +7,121 @@
|
|||
*/
|
||||
|
||||
import {EnvironmentInjector, ProviderToken, runInInjectionContext} from '@angular/core';
|
||||
import {concat, defer, from, MonoTypeOperatorFunction, Observable, of, OperatorFunction, pipe} from 'rxjs';
|
||||
import {
|
||||
concat,
|
||||
defer,
|
||||
from,
|
||||
MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
of,
|
||||
OperatorFunction,
|
||||
pipe,
|
||||
} from 'rxjs';
|
||||
import {concatMap, first, map, mergeMap, tap} from 'rxjs/operators';
|
||||
|
||||
import {ActivationStart, ChildActivationStart, Event} from '../events';
|
||||
import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Route} from '../models';
|
||||
import {
|
||||
CanActivateChildFn,
|
||||
CanActivateFn,
|
||||
CanDeactivateFn,
|
||||
CanLoadFn,
|
||||
CanMatchFn,
|
||||
Route,
|
||||
} from '../models';
|
||||
import {redirectingNavigationError} from '../navigation_canceling_error';
|
||||
import {NavigationTransition} from '../navigation_transition';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
|
||||
import {isUrlTree, UrlSegment, UrlSerializer, UrlTree} from '../url_tree';
|
||||
import {wrapIntoObservable} from '../utils/collection';
|
||||
import {getClosestRouteInjector} from '../utils/config';
|
||||
import {CanActivate, CanDeactivate, getCanActivateChild, getTokenOrFunctionIdentity} from '../utils/preactivation';
|
||||
import {isBoolean, isCanActivate, isCanActivateChild, isCanDeactivate, isCanLoad, isCanMatch} from '../utils/type_guards';
|
||||
import {
|
||||
CanActivate,
|
||||
CanDeactivate,
|
||||
getCanActivateChild,
|
||||
getTokenOrFunctionIdentity,
|
||||
} from '../utils/preactivation';
|
||||
import {
|
||||
isBoolean,
|
||||
isCanActivate,
|
||||
isCanActivateChild,
|
||||
isCanDeactivate,
|
||||
isCanLoad,
|
||||
isCanMatch,
|
||||
} from '../utils/type_guards';
|
||||
|
||||
import {prioritizedGuardValue} from './prioritized_guard_value';
|
||||
|
||||
export function checkGuards(injector: EnvironmentInjector, forwardEvent?: (evt: Event) => void):
|
||||
MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap(t => {
|
||||
const {targetSnapshot, currentSnapshot, guards: {canActivateChecks, canDeactivateChecks}} = t;
|
||||
export function checkGuards(
|
||||
injector: EnvironmentInjector,
|
||||
forwardEvent?: (evt: Event) => void,
|
||||
): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap((t) => {
|
||||
const {
|
||||
targetSnapshot,
|
||||
currentSnapshot,
|
||||
guards: {canActivateChecks, canDeactivateChecks},
|
||||
} = t;
|
||||
if (canDeactivateChecks.length === 0 && canActivateChecks.length === 0) {
|
||||
return of({...t, guardsResult: true});
|
||||
}
|
||||
|
||||
return runCanDeactivateChecks(canDeactivateChecks, targetSnapshot!, currentSnapshot, injector)
|
||||
.pipe(
|
||||
mergeMap(canDeactivate => {
|
||||
return canDeactivate && isBoolean(canDeactivate) ?
|
||||
runCanActivateChecks(targetSnapshot!, canActivateChecks, injector, forwardEvent) :
|
||||
of(canDeactivate);
|
||||
}),
|
||||
map(guardsResult => ({...t, guardsResult})));
|
||||
return runCanDeactivateChecks(
|
||||
canDeactivateChecks,
|
||||
targetSnapshot!,
|
||||
currentSnapshot,
|
||||
injector,
|
||||
).pipe(
|
||||
mergeMap((canDeactivate) => {
|
||||
return canDeactivate && isBoolean(canDeactivate)
|
||||
? runCanActivateChecks(targetSnapshot!, canActivateChecks, injector, forwardEvent)
|
||||
: of(canDeactivate);
|
||||
}),
|
||||
map((guardsResult) => ({...t, guardsResult})),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function runCanDeactivateChecks(
|
||||
checks: CanDeactivate[], futureRSS: RouterStateSnapshot, currRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector) {
|
||||
checks: CanDeactivate[],
|
||||
futureRSS: RouterStateSnapshot,
|
||||
currRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
) {
|
||||
return from(checks).pipe(
|
||||
mergeMap(
|
||||
check => runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector)),
|
||||
first(result => {
|
||||
mergeMap((check) =>
|
||||
runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector),
|
||||
),
|
||||
first(
|
||||
(result) => {
|
||||
return result !== true;
|
||||
}, true as boolean | UrlTree));
|
||||
},
|
||||
true as boolean | UrlTree,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function runCanActivateChecks(
|
||||
futureSnapshot: RouterStateSnapshot, checks: CanActivate[], injector: EnvironmentInjector,
|
||||
forwardEvent?: (evt: Event) => void) {
|
||||
futureSnapshot: RouterStateSnapshot,
|
||||
checks: CanActivate[],
|
||||
injector: EnvironmentInjector,
|
||||
forwardEvent?: (evt: Event) => void,
|
||||
) {
|
||||
return from(checks).pipe(
|
||||
concatMap((check: CanActivate) => {
|
||||
return concat(
|
||||
fireChildActivationStart(check.route.parent, forwardEvent),
|
||||
fireActivationStart(check.route, forwardEvent),
|
||||
runCanActivateChild(futureSnapshot, check.path, injector),
|
||||
runCanActivate(futureSnapshot, check.route, injector));
|
||||
}),
|
||||
first(result => {
|
||||
concatMap((check: CanActivate) => {
|
||||
return concat(
|
||||
fireChildActivationStart(check.route.parent, forwardEvent),
|
||||
fireActivationStart(check.route, forwardEvent),
|
||||
runCanActivateChild(futureSnapshot, check.path, injector),
|
||||
runCanActivate(futureSnapshot, check.route, injector),
|
||||
);
|
||||
}),
|
||||
first(
|
||||
(result) => {
|
||||
return result !== true;
|
||||
}, true as boolean | UrlTree));
|
||||
},
|
||||
true as boolean | UrlTree,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,8 +133,9 @@ function runCanActivateChecks(
|
|||
* `true` so checks continue to run.
|
||||
*/
|
||||
function fireActivationStart(
|
||||
snapshot: ActivatedRouteSnapshot|null,
|
||||
forwardEvent?: (evt: Event) => void): Observable<boolean> {
|
||||
snapshot: ActivatedRouteSnapshot | null,
|
||||
forwardEvent?: (evt: Event) => void,
|
||||
): Observable<boolean> {
|
||||
if (snapshot !== null && forwardEvent) {
|
||||
forwardEvent(new ActivationStart(snapshot));
|
||||
}
|
||||
|
|
@ -95,8 +151,9 @@ function fireActivationStart(
|
|||
* `true` so checks continue to run.
|
||||
*/
|
||||
function fireChildActivationStart(
|
||||
snapshot: ActivatedRouteSnapshot|null,
|
||||
forwardEvent?: (evt: Event) => void): Observable<boolean> {
|
||||
snapshot: ActivatedRouteSnapshot | null,
|
||||
forwardEvent?: (evt: Event) => void,
|
||||
): Observable<boolean> {
|
||||
if (snapshot !== null && forwardEvent) {
|
||||
forwardEvent(new ChildActivationStart(snapshot));
|
||||
}
|
||||
|
|
@ -104,49 +161,60 @@ function fireChildActivationStart(
|
|||
}
|
||||
|
||||
function runCanActivate(
|
||||
futureRSS: RouterStateSnapshot, futureARS: ActivatedRouteSnapshot,
|
||||
injector: EnvironmentInjector): Observable<boolean|UrlTree> {
|
||||
futureRSS: RouterStateSnapshot,
|
||||
futureARS: ActivatedRouteSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
): Observable<boolean | UrlTree> {
|
||||
const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null;
|
||||
if (!canActivate || canActivate.length === 0) return of(true);
|
||||
|
||||
const canActivateObservables =
|
||||
canActivate.map((canActivate: CanActivateFn|ProviderToken<unknown>) => {
|
||||
return defer(() => {
|
||||
const closestInjector = getClosestRouteInjector(futureARS) ?? injector;
|
||||
const guard = getTokenOrFunctionIdentity<CanActivate>(canActivate, closestInjector);
|
||||
const guardVal = isCanActivate(guard) ?
|
||||
guard.canActivate(futureARS, futureRSS) :
|
||||
runInInjectionContext(
|
||||
closestInjector, () => (guard as CanActivateFn)(futureARS, futureRSS));
|
||||
return wrapIntoObservable(guardVal).pipe(first());
|
||||
});
|
||||
const canActivateObservables = canActivate.map(
|
||||
(canActivate: CanActivateFn | ProviderToken<unknown>) => {
|
||||
return defer(() => {
|
||||
const closestInjector = getClosestRouteInjector(futureARS) ?? injector;
|
||||
const guard = getTokenOrFunctionIdentity<CanActivate>(canActivate, closestInjector);
|
||||
const guardVal = isCanActivate(guard)
|
||||
? guard.canActivate(futureARS, futureRSS)
|
||||
: runInInjectionContext(closestInjector, () =>
|
||||
(guard as CanActivateFn)(futureARS, futureRSS),
|
||||
);
|
||||
return wrapIntoObservable(guardVal).pipe(first());
|
||||
});
|
||||
},
|
||||
);
|
||||
return of(canActivateObservables).pipe(prioritizedGuardValue());
|
||||
}
|
||||
|
||||
function runCanActivateChild(
|
||||
futureRSS: RouterStateSnapshot, path: ActivatedRouteSnapshot[],
|
||||
injector: EnvironmentInjector): Observable<boolean|UrlTree> {
|
||||
futureRSS: RouterStateSnapshot,
|
||||
path: ActivatedRouteSnapshot[],
|
||||
injector: EnvironmentInjector,
|
||||
): Observable<boolean | UrlTree> {
|
||||
const futureARS = path[path.length - 1];
|
||||
|
||||
const canActivateChildGuards = path.slice(0, path.length - 1)
|
||||
.reverse()
|
||||
.map(p => getCanActivateChild(p))
|
||||
.filter(_ => _ !== null);
|
||||
const canActivateChildGuards = path
|
||||
.slice(0, path.length - 1)
|
||||
.reverse()
|
||||
.map((p) => getCanActivateChild(p))
|
||||
.filter((_) => _ !== null);
|
||||
|
||||
const canActivateChildGuardsMapped = canActivateChildGuards.map((d: any) => {
|
||||
return defer(() => {
|
||||
const guardsMapped =
|
||||
d.guards.map((canActivateChild: CanActivateChildFn|ProviderToken<unknown>) => {
|
||||
const closestInjector = getClosestRouteInjector(d.node) ?? injector;
|
||||
const guard = getTokenOrFunctionIdentity<{canActivateChild: CanActivateChildFn}>(
|
||||
canActivateChild, closestInjector);
|
||||
const guardVal = isCanActivateChild(guard) ?
|
||||
guard.canActivateChild(futureARS, futureRSS) :
|
||||
runInInjectionContext(
|
||||
closestInjector, () => (guard as CanActivateChildFn)(futureARS, futureRSS));
|
||||
return wrapIntoObservable(guardVal).pipe(first());
|
||||
});
|
||||
const guardsMapped = d.guards.map(
|
||||
(canActivateChild: CanActivateChildFn | ProviderToken<unknown>) => {
|
||||
const closestInjector = getClosestRouteInjector(d.node) ?? injector;
|
||||
const guard = getTokenOrFunctionIdentity<{canActivateChild: CanActivateChildFn}>(
|
||||
canActivateChild,
|
||||
closestInjector,
|
||||
);
|
||||
const guardVal = isCanActivateChild(guard)
|
||||
? guard.canActivateChild(futureARS, futureRSS)
|
||||
: runInInjectionContext(closestInjector, () =>
|
||||
(guard as CanActivateChildFn)(futureARS, futureRSS),
|
||||
);
|
||||
return wrapIntoObservable(guardVal).pipe(first());
|
||||
},
|
||||
);
|
||||
return of(guardsMapped).pipe(prioritizedGuardValue());
|
||||
});
|
||||
});
|
||||
|
|
@ -154,26 +222,33 @@ function runCanActivateChild(
|
|||
}
|
||||
|
||||
function runCanDeactivate(
|
||||
component: Object|null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot,
|
||||
futureRSS: RouterStateSnapshot, injector: EnvironmentInjector): Observable<boolean|UrlTree> {
|
||||
component: Object | null,
|
||||
currARS: ActivatedRouteSnapshot,
|
||||
currRSS: RouterStateSnapshot,
|
||||
futureRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
): Observable<boolean | UrlTree> {
|
||||
const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null;
|
||||
if (!canDeactivate || canDeactivate.length === 0) return of(true);
|
||||
const canDeactivateObservables = canDeactivate.map((c: any) => {
|
||||
const closestInjector = getClosestRouteInjector(currARS) ?? injector;
|
||||
const guard = getTokenOrFunctionIdentity<any>(c, closestInjector);
|
||||
const guardVal = isCanDeactivate(guard) ?
|
||||
guard.canDeactivate(component, currARS, currRSS, futureRSS) :
|
||||
runInInjectionContext(
|
||||
closestInjector,
|
||||
() => (guard as CanDeactivateFn<any>)(component, currARS, currRSS, futureRSS));
|
||||
const guardVal = isCanDeactivate(guard)
|
||||
? guard.canDeactivate(component, currARS, currRSS, futureRSS)
|
||||
: runInInjectionContext(closestInjector, () =>
|
||||
(guard as CanDeactivateFn<any>)(component, currARS, currRSS, futureRSS),
|
||||
);
|
||||
return wrapIntoObservable(guardVal).pipe(first());
|
||||
});
|
||||
return of(canDeactivateObservables).pipe(prioritizedGuardValue());
|
||||
}
|
||||
|
||||
export function runCanLoadGuards(
|
||||
injector: EnvironmentInjector, route: Route, segments: UrlSegment[],
|
||||
urlSerializer: UrlSerializer): Observable<boolean> {
|
||||
injector: EnvironmentInjector,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
urlSerializer: UrlSerializer,
|
||||
): Observable<boolean> {
|
||||
const canLoad = route.canLoad;
|
||||
if (canLoad === undefined || canLoad.length === 0) {
|
||||
return of(true);
|
||||
|
|
@ -181,48 +256,44 @@ export function runCanLoadGuards(
|
|||
|
||||
const canLoadObservables = canLoad.map((injectionToken: any) => {
|
||||
const guard = getTokenOrFunctionIdentity<any>(injectionToken, injector);
|
||||
const guardVal = isCanLoad(guard) ?
|
||||
guard.canLoad(route, segments) :
|
||||
runInInjectionContext(injector, () => (guard as CanLoadFn)(route, segments));
|
||||
const guardVal = isCanLoad(guard)
|
||||
? guard.canLoad(route, segments)
|
||||
: runInInjectionContext(injector, () => (guard as CanLoadFn)(route, segments));
|
||||
return wrapIntoObservable(guardVal);
|
||||
});
|
||||
|
||||
return of(canLoadObservables)
|
||||
.pipe(
|
||||
prioritizedGuardValue(),
|
||||
redirectIfUrlTree(urlSerializer),
|
||||
);
|
||||
return of(canLoadObservables).pipe(prioritizedGuardValue(), redirectIfUrlTree(urlSerializer));
|
||||
}
|
||||
|
||||
function redirectIfUrlTree(urlSerializer: UrlSerializer):
|
||||
OperatorFunction<UrlTree|boolean, boolean> {
|
||||
function redirectIfUrlTree(
|
||||
urlSerializer: UrlSerializer,
|
||||
): OperatorFunction<UrlTree | boolean, boolean> {
|
||||
return pipe(
|
||||
tap((result: UrlTree|boolean) => {
|
||||
if (!isUrlTree(result)) return;
|
||||
tap((result: UrlTree | boolean) => {
|
||||
if (!isUrlTree(result)) return;
|
||||
|
||||
throw redirectingNavigationError(urlSerializer, result);
|
||||
}),
|
||||
map(result => result === true),
|
||||
throw redirectingNavigationError(urlSerializer, result);
|
||||
}),
|
||||
map((result) => result === true),
|
||||
);
|
||||
}
|
||||
|
||||
export function runCanMatchGuards(
|
||||
injector: EnvironmentInjector, route: Route, segments: UrlSegment[],
|
||||
urlSerializer: UrlSerializer): Observable<boolean> {
|
||||
injector: EnvironmentInjector,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
urlSerializer: UrlSerializer,
|
||||
): Observable<boolean> {
|
||||
const canMatch = route.canMatch;
|
||||
if (!canMatch || canMatch.length === 0) return of(true);
|
||||
|
||||
const canMatchObservables = canMatch.map(injectionToken => {
|
||||
const canMatchObservables = canMatch.map((injectionToken) => {
|
||||
const guard = getTokenOrFunctionIdentity(injectionToken, injector);
|
||||
const guardVal = isCanMatch(guard) ?
|
||||
guard.canMatch(route, segments) :
|
||||
runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments));
|
||||
const guardVal = isCanMatch(guard)
|
||||
? guard.canMatch(route, segments)
|
||||
: runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments));
|
||||
return wrapIntoObservable(guardVal);
|
||||
});
|
||||
|
||||
return of(canMatchObservables)
|
||||
.pipe(
|
||||
prioritizedGuardValue(),
|
||||
redirectIfUrlTree(urlSerializer),
|
||||
);
|
||||
return of(canMatchObservables).pipe(prioritizedGuardValue(), redirectIfUrlTree(urlSerializer));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,31 +14,34 @@ import {UrlTree} from '../url_tree';
|
|||
const INITIAL_VALUE = /* @__PURE__ */ Symbol('INITIAL_VALUE');
|
||||
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;
|
||||
|
||||
export function prioritizedGuardValue():
|
||||
OperatorFunction<Observable<boolean|UrlTree>[], boolean|UrlTree> {
|
||||
return switchMap(obs => {
|
||||
return combineLatest(obs.map(o => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES))))
|
||||
.pipe(
|
||||
map((results: INTERIM_VALUES[]) => {
|
||||
for (const result of results) {
|
||||
if (result === true) {
|
||||
// If result is true, check the next one
|
||||
continue;
|
||||
} else if (result === INITIAL_VALUE) {
|
||||
// If guard has not finished, we need to stop processing.
|
||||
return INITIAL_VALUE;
|
||||
} else if (result === false || result instanceof UrlTree) {
|
||||
// Result finished and was not true. Return the result.
|
||||
// Note that we only allow false/UrlTree. Other values are considered invalid and
|
||||
// ignored.
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Everything resolved to true. Return true.
|
||||
return true;
|
||||
}),
|
||||
filter((item): item is boolean|UrlTree => item !== INITIAL_VALUE),
|
||||
take(1),
|
||||
);
|
||||
export function prioritizedGuardValue(): OperatorFunction<
|
||||
Observable<boolean | UrlTree>[],
|
||||
boolean | UrlTree
|
||||
> {
|
||||
return switchMap((obs) => {
|
||||
return combineLatest(
|
||||
obs.map((o) => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES))),
|
||||
).pipe(
|
||||
map((results: INTERIM_VALUES[]) => {
|
||||
for (const result of results) {
|
||||
if (result === true) {
|
||||
// If result is true, check the next one
|
||||
continue;
|
||||
} else if (result === INITIAL_VALUE) {
|
||||
// If guard has not finished, we need to stop processing.
|
||||
return INITIAL_VALUE;
|
||||
} else if (result === false || result instanceof UrlTree) {
|
||||
// Result finished and was not true. Return the result.
|
||||
// Note that we only allow false/UrlTree. Other values are considered invalid and
|
||||
// ignored.
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Everything resolved to true. Return true.
|
||||
return true;
|
||||
}),
|
||||
filter((item): item is boolean | UrlTree => item !== INITIAL_VALUE),
|
||||
take(1),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,26 @@ import {RouterConfigLoader} from '../router_config_loader';
|
|||
import {UrlSerializer} from '../url_tree';
|
||||
|
||||
export function recognize(
|
||||
injector: EnvironmentInjector, configLoader: RouterConfigLoader,
|
||||
rootComponentType: Type<any>|null, config: Route[], serializer: UrlSerializer,
|
||||
paramsInheritanceStrategy: 'emptyOnly'|
|
||||
'always'): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap(
|
||||
t => recognizeFn(
|
||||
injector, configLoader, rootComponentType, config, t.extractedUrl, serializer,
|
||||
paramsInheritanceStrategy)
|
||||
.pipe(map(({state: targetSnapshot, tree: urlAfterRedirects}) => {
|
||||
return {...t, targetSnapshot, urlAfterRedirects};
|
||||
})));
|
||||
injector: EnvironmentInjector,
|
||||
configLoader: RouterConfigLoader,
|
||||
rootComponentType: Type<any> | null,
|
||||
config: Route[],
|
||||
serializer: UrlSerializer,
|
||||
paramsInheritanceStrategy: 'emptyOnly' | 'always',
|
||||
): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap((t) =>
|
||||
recognizeFn(
|
||||
injector,
|
||||
configLoader,
|
||||
rootComponentType,
|
||||
config,
|
||||
t.extractedUrl,
|
||||
serializer,
|
||||
paramsInheritanceStrategy,
|
||||
).pipe(
|
||||
map(({state: targetSnapshot, tree: urlAfterRedirects}) => {
|
||||
return {...t, targetSnapshot, urlAfterRedirects};
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import {catchError, concatMap, first, map, mapTo, mergeMap, takeLast, tap} from
|
|||
|
||||
import {ResolveData} from '../models';
|
||||
import {NavigationTransition} from '../navigation_transition';
|
||||
import {ActivatedRouteSnapshot, getInherited, hasStaticTitle, RouterStateSnapshot} from '../router_state';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
getInherited,
|
||||
hasStaticTitle,
|
||||
RouterStateSnapshot,
|
||||
} from '../router_state';
|
||||
import {RouteTitleKey} from '../shared';
|
||||
import {getDataKeys, wrapIntoObservable} from '../utils/collection';
|
||||
import {getClosestRouteInjector} from '../utils/config';
|
||||
|
|
@ -20,10 +25,14 @@ import {getTokenOrFunctionIdentity} from '../utils/preactivation';
|
|||
import {isEmptyError} from '../utils/type_guards';
|
||||
|
||||
export function resolveData(
|
||||
paramsInheritanceStrategy: 'emptyOnly'|'always',
|
||||
injector: EnvironmentInjector): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap(t => {
|
||||
const {targetSnapshot, guards: {canActivateChecks}} = t;
|
||||
paramsInheritanceStrategy: 'emptyOnly' | 'always',
|
||||
injector: EnvironmentInjector,
|
||||
): MonoTypeOperatorFunction<NavigationTransition> {
|
||||
return mergeMap((t) => {
|
||||
const {
|
||||
targetSnapshot,
|
||||
guards: {canActivateChecks},
|
||||
} = t;
|
||||
|
||||
if (!canActivateChecks.length) {
|
||||
return of(t);
|
||||
|
|
@ -31,7 +40,7 @@ export function resolveData(
|
|||
// Iterating a Set in javascript happens in insertion order so it is safe to use a `Set` to
|
||||
// preserve the correct order that the resolvers should run in.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#description
|
||||
const routesWithResolversToRun = new Set(canActivateChecks.map(check => check.route));
|
||||
const routesWithResolversToRun = new Set(canActivateChecks.map((check) => check.route));
|
||||
const routesNeedingDataUpdates = new Set<ActivatedRouteSnapshot>();
|
||||
for (const route of routesWithResolversToRun) {
|
||||
if (routesNeedingDataUpdates.has(route)) {
|
||||
|
|
@ -43,20 +52,19 @@ export function resolveData(
|
|||
}
|
||||
}
|
||||
let routesProcessed = 0;
|
||||
return from(routesNeedingDataUpdates)
|
||||
.pipe(
|
||||
concatMap(route => {
|
||||
if (routesWithResolversToRun.has(route)) {
|
||||
return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector);
|
||||
} else {
|
||||
route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve;
|
||||
return of(void 0);
|
||||
}
|
||||
}),
|
||||
tap(() => routesProcessed++),
|
||||
takeLast(1),
|
||||
mergeMap(_ => routesProcessed === routesNeedingDataUpdates.size ? of(t) : EMPTY),
|
||||
);
|
||||
return from(routesNeedingDataUpdates).pipe(
|
||||
concatMap((route) => {
|
||||
if (routesWithResolversToRun.has(route)) {
|
||||
return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector);
|
||||
} else {
|
||||
route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve;
|
||||
return of(void 0);
|
||||
}
|
||||
}),
|
||||
tap(() => routesProcessed++),
|
||||
takeLast(1),
|
||||
mergeMap((_) => (routesProcessed === routesNeedingDataUpdates.size ? of(t) : EMPTY)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -64,52 +72,66 @@ export function resolveData(
|
|||
* Returns the `ActivatedRouteSnapshot` tree as an array, using DFS to traverse the route tree.
|
||||
*/
|
||||
function flattenRouteTree(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot[] {
|
||||
const descendants = route.children.map(child => flattenRouteTree(child)).flat();
|
||||
const descendants = route.children.map((child) => flattenRouteTree(child)).flat();
|
||||
return [route, ...descendants];
|
||||
}
|
||||
|
||||
function runResolve(
|
||||
futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot,
|
||||
paramsInheritanceStrategy: 'emptyOnly'|'always', injector: EnvironmentInjector) {
|
||||
futureARS: ActivatedRouteSnapshot,
|
||||
futureRSS: RouterStateSnapshot,
|
||||
paramsInheritanceStrategy: 'emptyOnly' | 'always',
|
||||
injector: EnvironmentInjector,
|
||||
) {
|
||||
const config = futureARS.routeConfig;
|
||||
const resolve = futureARS._resolve;
|
||||
if (config?.title !== undefined && !hasStaticTitle(config)) {
|
||||
resolve[RouteTitleKey] = config.title;
|
||||
}
|
||||
return resolveNode(resolve, futureARS, futureRSS, injector).pipe(map((resolvedData: any) => {
|
||||
futureARS._resolvedData = resolvedData;
|
||||
futureARS.data = getInherited(futureARS, futureARS.parent, paramsInheritanceStrategy).resolve;
|
||||
return null;
|
||||
}));
|
||||
return resolveNode(resolve, futureARS, futureRSS, injector).pipe(
|
||||
map((resolvedData: any) => {
|
||||
futureARS._resolvedData = resolvedData;
|
||||
futureARS.data = getInherited(futureARS, futureARS.parent, paramsInheritanceStrategy).resolve;
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveNode(
|
||||
resolve: ResolveData, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector): Observable<any> {
|
||||
resolve: ResolveData,
|
||||
futureARS: ActivatedRouteSnapshot,
|
||||
futureRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
): Observable<any> {
|
||||
const keys = getDataKeys(resolve);
|
||||
if (keys.length === 0) {
|
||||
return of({});
|
||||
}
|
||||
const data: {[k: string|symbol]: any} = {};
|
||||
const data: {[k: string | symbol]: any} = {};
|
||||
return from(keys).pipe(
|
||||
mergeMap(
|
||||
key => getResolver(resolve[key], futureARS, futureRSS, injector)
|
||||
.pipe(first(), tap((value: any) => {
|
||||
data[key] = value;
|
||||
}))),
|
||||
takeLast(1),
|
||||
mapTo(data),
|
||||
catchError((e: unknown) => isEmptyError(e as Error) ? EMPTY : throwError(e)),
|
||||
mergeMap((key) =>
|
||||
getResolver(resolve[key], futureARS, futureRSS, injector).pipe(
|
||||
first(),
|
||||
tap((value: any) => {
|
||||
data[key] = value;
|
||||
}),
|
||||
),
|
||||
),
|
||||
takeLast(1),
|
||||
mapTo(data),
|
||||
catchError((e: unknown) => (isEmptyError(e as Error) ? EMPTY : throwError(e))),
|
||||
);
|
||||
}
|
||||
|
||||
function getResolver(
|
||||
injectionToken: ProviderToken<any>|Function, futureARS: ActivatedRouteSnapshot,
|
||||
futureRSS: RouterStateSnapshot, injector: EnvironmentInjector): Observable<any> {
|
||||
injectionToken: ProviderToken<any> | Function,
|
||||
futureARS: ActivatedRouteSnapshot,
|
||||
futureRSS: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
): Observable<any> {
|
||||
const closestInjector = getClosestRouteInjector(futureARS) ?? injector;
|
||||
const resolver = getTokenOrFunctionIdentity(injectionToken, closestInjector);
|
||||
const resolverValue = resolver.resolve ?
|
||||
resolver.resolve(futureARS, futureRSS) :
|
||||
runInInjectionContext(closestInjector, () => resolver(futureARS, futureRSS));
|
||||
const resolverValue = resolver.resolve
|
||||
? resolver.resolve(futureARS, futureRSS)
|
||||
: runInInjectionContext(closestInjector, () => resolver(futureARS, futureRSS));
|
||||
return wrapIntoObservable(resolverValue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ import {map, switchMap} from 'rxjs/operators';
|
|||
* the `tap` operator, but if the side effectful `next` function returns an ObservableInput,
|
||||
* it will wait before continuing with the original value.
|
||||
*/
|
||||
export function switchTap<T>(next: (x: T) => void|ObservableInput<any>):
|
||||
MonoTypeOperatorFunction<T> {
|
||||
return switchMap(v => {
|
||||
export function switchTap<T>(
|
||||
next: (x: T) => void | ObservableInput<any>,
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return switchMap((v) => {
|
||||
const nextResult = next(v);
|
||||
if (nextResult) {
|
||||
return from(nextResult).pipe(map(() => v));
|
||||
|
|
|
|||
|
|
@ -43,12 +43,12 @@ export abstract class TitleStrategy {
|
|||
/**
|
||||
* @returns The `title` of the deepest primary route.
|
||||
*/
|
||||
buildTitle(snapshot: RouterStateSnapshot): string|undefined {
|
||||
let pageTitle: string|undefined;
|
||||
let route: ActivatedRouteSnapshot|undefined = snapshot.root;
|
||||
buildTitle(snapshot: RouterStateSnapshot): string | undefined {
|
||||
let pageTitle: string | undefined;
|
||||
let route: ActivatedRouteSnapshot | undefined = snapshot.root;
|
||||
while (route !== undefined) {
|
||||
pageTitle = this.getResolvedTitleForRoute(route) ?? pageTitle;
|
||||
route = route.children.find(child => child.outlet === PRIMARY_OUTLET);
|
||||
route = route.children.find((child) => child.outlet === PRIMARY_OUTLET);
|
||||
}
|
||||
return pageTitle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
export {ɵEmptyOutletComponent} from './components/empty_outlet';
|
||||
export {RestoredState as ɵRestoredState} from './navigation_transition';
|
||||
export {loadChildren as ɵloadChildren} from './router_config_loader';
|
||||
|
|
|
|||
|
|
@ -6,8 +6,30 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {HashLocationStrategy, LOCATION_INITIALIZED, LocationStrategy, ViewportScroller} from '@angular/common';
|
||||
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, runInInjectionContext, Type} from '@angular/core';
|
||||
import {
|
||||
HashLocationStrategy,
|
||||
LOCATION_INITIALIZED,
|
||||
LocationStrategy,
|
||||
ViewportScroller,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
APP_BOOTSTRAP_LISTENER,
|
||||
APP_INITIALIZER,
|
||||
ApplicationRef,
|
||||
ComponentRef,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentInjector,
|
||||
EnvironmentProviders,
|
||||
inject,
|
||||
InjectFlags,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
makeEnvironmentProviders,
|
||||
NgZone,
|
||||
Provider,
|
||||
runInInjectionContext,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import {of, Subject} from 'rxjs';
|
||||
|
||||
import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet';
|
||||
|
|
@ -22,8 +44,12 @@ import {ROUTER_SCROLLER, RouterScroller} from './router_scroller';
|
|||
import {ActivatedRoute} from './router_state';
|
||||
import {UrlSerializer} from './url_tree';
|
||||
import {afterNextNavigation} from './utils/navigations';
|
||||
import {CREATE_VIEW_TRANSITION, createViewTransition, VIEW_TRANSITION_OPTIONS, ViewTransitionsFeatureOptions} from './utils/view_transition';
|
||||
|
||||
import {
|
||||
CREATE_VIEW_TRANSITION,
|
||||
createViewTransition,
|
||||
VIEW_TRANSITION_OPTIONS,
|
||||
ViewTransitionsFeatureOptions,
|
||||
} from './utils/view_transition';
|
||||
|
||||
/**
|
||||
* Sets up providers necessary to enable `Router` functionality for the application.
|
||||
|
|
@ -64,12 +90,12 @@ import {CREATE_VIEW_TRANSITION, createViewTransition, VIEW_TRANSITION_OPTIONS, V
|
|||
export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
{provide: ROUTES, multi: true, useValue: routes},
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ?
|
||||
{provide: ROUTER_IS_PROVIDED, useValue: true} :
|
||||
[],
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? {provide: ROUTER_IS_PROVIDED, useValue: true}
|
||||
: [],
|
||||
{provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
|
||||
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: getBootstrapListener},
|
||||
features.map(feature => feature.ɵproviders),
|
||||
features.map((feature) => feature.ɵproviders),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -91,17 +117,20 @@ export interface RouterFeature<FeatureKind extends RouterFeatureKind> {
|
|||
* Helper function to create an object that represents a Router feature.
|
||||
*/
|
||||
function routerFeature<FeatureKind extends RouterFeatureKind>(
|
||||
kind: FeatureKind, providers: Provider[]): RouterFeature<FeatureKind> {
|
||||
kind: FeatureKind,
|
||||
providers: Provider[],
|
||||
): RouterFeature<FeatureKind> {
|
||||
return {ɵkind: kind, ɵproviders: providers};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An Injection token used to indicate whether `provideRouter` or `RouterModule.forRoot` was ever
|
||||
* called.
|
||||
*/
|
||||
export const ROUTER_IS_PROVIDED =
|
||||
new InjectionToken<boolean>('', {providedIn: 'root', factory: () => false});
|
||||
export const ROUTER_IS_PROVIDED = new InjectionToken<boolean>('', {
|
||||
providedIn: 'root',
|
||||
factory: () => false,
|
||||
});
|
||||
|
||||
const routerIsProvidedDevModeCheck = {
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
|
|
@ -110,11 +139,12 @@ const routerIsProvidedDevModeCheck = {
|
|||
return () => {
|
||||
if (!inject(ROUTER_IS_PROVIDED)) {
|
||||
console.warn(
|
||||
'`provideRoutes` was called without `provideRouter` or `RouterModule.forRoot`. ' +
|
||||
'This is likely a mistake.');
|
||||
'`provideRoutes` was called without `provideRouter` or `RouterModule.forRoot`. ' +
|
||||
'This is likely a mistake.',
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -137,7 +167,7 @@ const routerIsProvidedDevModeCheck = {
|
|||
export function provideRoutes(routes: Routes): Provider[] {
|
||||
return [
|
||||
{provide: ROUTES, multi: true, useValue: routes},
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? routerIsProvidedDevModeCheck : [],
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? routerIsProvidedDevModeCheck : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -176,18 +206,21 @@ export type InMemoryScrollingFeature = RouterFeature<RouterFeatureKind.InMemoryS
|
|||
* `InMemoryScrollingOptions` for additional information.
|
||||
* @returns A set of providers for use with `provideRouter`.
|
||||
*/
|
||||
export function withInMemoryScrolling(options: InMemoryScrollingOptions = {}):
|
||||
InMemoryScrollingFeature {
|
||||
const providers = [{
|
||||
provide: ROUTER_SCROLLER,
|
||||
useFactory: () => {
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const zone = inject(NgZone);
|
||||
const transitions = inject(NavigationTransitions);
|
||||
const urlSerializer = inject(UrlSerializer);
|
||||
return new RouterScroller(urlSerializer, transitions, viewportScroller, zone, options);
|
||||
export function withInMemoryScrolling(
|
||||
options: InMemoryScrollingOptions = {},
|
||||
): InMemoryScrollingFeature {
|
||||
const providers = [
|
||||
{
|
||||
provide: ROUTER_SCROLLER,
|
||||
useFactory: () => {
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const zone = inject(NgZone);
|
||||
const transitions = inject(NavigationTransitions);
|
||||
const urlSerializer = inject(UrlSerializer);
|
||||
return new RouterScroller(urlSerializer, transitions, viewportScroller, zone, options);
|
||||
},
|
||||
},
|
||||
}];
|
||||
];
|
||||
return routerFeature(RouterFeatureKind.InMemoryScrollingFeature, providers);
|
||||
}
|
||||
|
||||
|
|
@ -224,11 +257,13 @@ export function getBootstrapListener() {
|
|||
* to the activation phase.
|
||||
*/
|
||||
const BOOTSTRAP_DONE = new InjectionToken<Subject<void>>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'bootstrap done indicator' : '', {
|
||||
factory: () => {
|
||||
return new Subject<void>();
|
||||
}
|
||||
});
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'bootstrap done indicator' : '',
|
||||
{
|
||||
factory: () => {
|
||||
return new Subject<void>();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* This and the INITIAL_NAVIGATION token are used internally only. The public API side of this is
|
||||
|
|
@ -254,8 +289,9 @@ const enum InitialNavigation {
|
|||
}
|
||||
|
||||
const INITIAL_NAVIGATION = new InjectionToken<InitialNavigation>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'initial navigation' : '',
|
||||
{providedIn: 'root', factory: () => InitialNavigation.EnabledNonBlocking});
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'initial navigation' : '',
|
||||
{providedIn: 'root', factory: () => InitialNavigation.EnabledNonBlocking},
|
||||
);
|
||||
|
||||
/**
|
||||
* A type alias for providers returned by `withEnabledBlockingInitialNavigation` for use with
|
||||
|
|
@ -267,7 +303,7 @@ const INITIAL_NAVIGATION = new InjectionToken<InitialNavigation>(
|
|||
* @publicApi
|
||||
*/
|
||||
export type EnabledBlockingInitialNavigationFeature =
|
||||
RouterFeature<RouterFeatureKind.EnabledBlockingInitialNavigationFeature>;
|
||||
RouterFeature<RouterFeatureKind.EnabledBlockingInitialNavigationFeature>;
|
||||
|
||||
/**
|
||||
* A type alias for providers returned by `withEnabledBlockingInitialNavigation` or
|
||||
|
|
@ -280,7 +316,8 @@ export type EnabledBlockingInitialNavigationFeature =
|
|||
* @publicApi
|
||||
*/
|
||||
export type InitialNavigationFeature =
|
||||
EnabledBlockingInitialNavigationFeature|DisabledInitialNavigationFeature;
|
||||
| EnabledBlockingInitialNavigationFeature
|
||||
| DisabledInitialNavigationFeature;
|
||||
|
||||
/**
|
||||
* Configures initial navigation to start before the root component is created.
|
||||
|
|
@ -315,12 +352,14 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa
|
|||
multi: true,
|
||||
deps: [Injector],
|
||||
useFactory: (injector: Injector) => {
|
||||
const locationInitialized: Promise<any> =
|
||||
injector.get(LOCATION_INITIALIZED, Promise.resolve());
|
||||
const locationInitialized: Promise<any> = injector.get(
|
||||
LOCATION_INITIALIZED,
|
||||
Promise.resolve(),
|
||||
);
|
||||
|
||||
return () => {
|
||||
return locationInitialized.then(() => {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
const router = injector.get(Router);
|
||||
const bootstrapDone = injector.get(BOOTSTRAP_DONE);
|
||||
afterNextNavigation(router, () => {
|
||||
|
|
@ -340,7 +379,7 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa
|
|||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
return routerFeature(RouterFeatureKind.EnabledBlockingInitialNavigationFeature, providers);
|
||||
|
|
@ -356,7 +395,7 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa
|
|||
* @publicApi
|
||||
*/
|
||||
export type DisabledInitialNavigationFeature =
|
||||
RouterFeature<RouterFeatureKind.DisabledInitialNavigationFeature>;
|
||||
RouterFeature<RouterFeatureKind.DisabledInitialNavigationFeature>;
|
||||
|
||||
/**
|
||||
* Disables initial navigation.
|
||||
|
|
@ -394,9 +433,9 @@ export function withDisabledInitialNavigation(): DisabledInitialNavigationFeatur
|
|||
return () => {
|
||||
router.setUpLocationChangeListener();
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{provide: INITIAL_NAVIGATION, useValue: InitialNavigation.Disabled}
|
||||
{provide: INITIAL_NAVIGATION, useValue: InitialNavigation.Disabled},
|
||||
];
|
||||
return routerFeature(RouterFeatureKind.DisabledInitialNavigationFeature, providers);
|
||||
}
|
||||
|
|
@ -438,21 +477,24 @@ export type DebugTracingFeature = RouterFeature<RouterFeatureKind.DebugTracingFe
|
|||
export function withDebugTracing(): DebugTracingFeature {
|
||||
let providers: Provider[] = [];
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
providers = [{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: () => {
|
||||
const router = inject(Router);
|
||||
return () => router.events.subscribe((e: Event) => {
|
||||
// tslint:disable:no-console
|
||||
console.group?.(`Router Event: ${(<any>e.constructor).name}`);
|
||||
console.log(stringifyEvent(e));
|
||||
console.log(e);
|
||||
console.groupEnd?.();
|
||||
// tslint:enable:no-console
|
||||
});
|
||||
}
|
||||
}];
|
||||
providers = [
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: () => {
|
||||
const router = inject(Router);
|
||||
return () =>
|
||||
router.events.subscribe((e: Event) => {
|
||||
// tslint:disable:no-console
|
||||
console.group?.(`Router Event: ${(<any>e.constructor).name}`);
|
||||
console.log(stringifyEvent(e));
|
||||
console.log(e);
|
||||
console.groupEnd?.();
|
||||
// tslint:enable:no-console
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
providers = [];
|
||||
}
|
||||
|
|
@ -460,7 +502,8 @@ export function withDebugTracing(): DebugTracingFeature {
|
|||
}
|
||||
|
||||
const ROUTER_PRELOADER = new InjectionToken<RouterPreloader>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'router preloader' : '');
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'router preloader' : '',
|
||||
);
|
||||
|
||||
/**
|
||||
* A type alias that represents a feature which enables preloading in Router.
|
||||
|
|
@ -516,7 +559,7 @@ export function withPreloading(preloadingStrategy: Type<PreloadingStrategy>): Pr
|
|||
* @publicApi
|
||||
*/
|
||||
export type RouterConfigurationFeature =
|
||||
RouterFeature<RouterFeatureKind.RouterConfigurationFeature>;
|
||||
RouterFeature<RouterFeatureKind.RouterConfigurationFeature>;
|
||||
|
||||
/**
|
||||
* Allows to provide extra parameters to configure Router.
|
||||
|
|
@ -546,9 +589,7 @@ export type RouterConfigurationFeature =
|
|||
* @publicApi
|
||||
*/
|
||||
export function withRouterConfig(options: RouterConfigOptions): RouterConfigurationFeature {
|
||||
const providers = [
|
||||
{provide: ROUTER_CONFIGURATION, useValue: options},
|
||||
];
|
||||
const providers = [{provide: ROUTER_CONFIGURATION, useValue: options}];
|
||||
return routerFeature(RouterFeatureKind.RouterConfigurationFeature, providers);
|
||||
}
|
||||
|
||||
|
|
@ -587,9 +628,7 @@ export type RouterHashLocationFeature = RouterFeature<RouterFeatureKind.RouterHa
|
|||
* @publicApi
|
||||
*/
|
||||
export function withHashLocation(): RouterHashLocationFeature {
|
||||
const providers = [
|
||||
{provide: LocationStrategy, useClass: HashLocationStrategy},
|
||||
];
|
||||
const providers = [{provide: LocationStrategy, useClass: HashLocationStrategy}];
|
||||
return routerFeature(RouterFeatureKind.RouterHashLocationFeature, providers);
|
||||
}
|
||||
|
||||
|
|
@ -602,7 +641,7 @@ export function withHashLocation(): RouterHashLocationFeature {
|
|||
* @publicApi
|
||||
*/
|
||||
export type NavigationErrorHandlerFeature =
|
||||
RouterFeature<RouterFeatureKind.NavigationErrorHandlerFeature>;
|
||||
RouterFeature<RouterFeatureKind.NavigationErrorHandlerFeature>;
|
||||
|
||||
/**
|
||||
* Subscribes to the Router's navigation events and calls the given function when a
|
||||
|
|
@ -634,20 +673,23 @@ export type NavigationErrorHandlerFeature =
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function withNavigationErrorHandler(fn: (error: NavigationError) => void):
|
||||
NavigationErrorHandlerFeature {
|
||||
const providers = [{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
const injector = inject(EnvironmentInjector);
|
||||
inject(Router).events.subscribe((e) => {
|
||||
if (e instanceof NavigationError) {
|
||||
runInInjectionContext(injector, () => fn(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
}];
|
||||
export function withNavigationErrorHandler(
|
||||
fn: (error: NavigationError) => void,
|
||||
): NavigationErrorHandlerFeature {
|
||||
const providers = [
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
const injector = inject(EnvironmentInjector);
|
||||
inject(Router).events.subscribe((e) => {
|
||||
if (e instanceof NavigationError) {
|
||||
runInInjectionContext(injector, () => fn(e));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
return routerFeature(RouterFeatureKind.NavigationErrorHandlerFeature, providers);
|
||||
}
|
||||
|
||||
|
|
@ -660,7 +702,7 @@ export function withNavigationErrorHandler(fn: (error: NavigationError) => void)
|
|||
* @publicApi
|
||||
*/
|
||||
export type ComponentInputBindingFeature =
|
||||
RouterFeature<RouterFeatureKind.ComponentInputBindingFeature>;
|
||||
RouterFeature<RouterFeatureKind.ComponentInputBindingFeature>;
|
||||
|
||||
/**
|
||||
* A type alias for providers returned by `withViewTransitions` for use with `provideRouter`.
|
||||
|
|
@ -728,13 +770,14 @@ export function withComponentInputBinding(): ComponentInputBindingFeature {
|
|||
* @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
|
||||
* @experimental
|
||||
*/
|
||||
export function withViewTransitions(options?: ViewTransitionsFeatureOptions):
|
||||
ViewTransitionsFeature {
|
||||
export function withViewTransitions(
|
||||
options?: ViewTransitionsFeatureOptions,
|
||||
): ViewTransitionsFeature {
|
||||
const providers = [
|
||||
{provide: CREATE_VIEW_TRANSITION, useValue: createViewTransition},
|
||||
{
|
||||
provide: VIEW_TRANSITION_OPTIONS,
|
||||
useValue: {skipNextTransition: !!options?.skipInitialTransition, ...options}
|
||||
useValue: {skipNextTransition: !!options?.skipInitialTransition, ...options},
|
||||
},
|
||||
];
|
||||
return routerFeature(RouterFeatureKind.ViewTransitionsFeature, providers);
|
||||
|
|
@ -750,9 +793,16 @@ export function withViewTransitions(options?: ViewTransitionsFeatureOptions):
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export type RouterFeatures = PreloadingFeature|DebugTracingFeature|InitialNavigationFeature|
|
||||
InMemoryScrollingFeature|RouterConfigurationFeature|NavigationErrorHandlerFeature|
|
||||
ComponentInputBindingFeature|ViewTransitionsFeature|RouterHashLocationFeature;
|
||||
export type RouterFeatures =
|
||||
| PreloadingFeature
|
||||
| DebugTracingFeature
|
||||
| InitialNavigationFeature
|
||||
| InMemoryScrollingFeature
|
||||
| RouterConfigurationFeature
|
||||
| NavigationErrorHandlerFeature
|
||||
| ComponentInputBindingFeature
|
||||
| ViewTransitionsFeature
|
||||
| RouterHashLocationFeature;
|
||||
|
||||
/**
|
||||
* The list of features as an enum to uniquely type each feature.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,18 @@
|
|||
|
||||
import {EnvironmentInjector, Type, ɵRuntimeError as RuntimeError} from '@angular/core';
|
||||
import {from, Observable, of} from 'rxjs';
|
||||
import {catchError, concatMap, defaultIfEmpty, first, last, map, mergeMap, scan, switchMap, tap} from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
defaultIfEmpty,
|
||||
first,
|
||||
last,
|
||||
map,
|
||||
mergeMap,
|
||||
scan,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {AbsoluteRedirect, ApplyRedirects, canLoadFails, noMatch, NoMatch} from './apply_redirects';
|
||||
import {createUrlTreeFromSnapshot} from './create_url_tree';
|
||||
|
|
@ -16,11 +27,22 @@ import {RuntimeErrorCode} from './errors';
|
|||
import {Data, LoadedRouterConfig, ResolveData, Route, Routes} from './models';
|
||||
import {runCanLoadGuards} from './operators/check_guards';
|
||||
import {RouterConfigLoader} from './router_config_loader';
|
||||
import {ActivatedRouteSnapshot, getInherited, ParamsInheritanceStrategy, RouterStateSnapshot} from './router_state';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
getInherited,
|
||||
ParamsInheritanceStrategy,
|
||||
RouterStateSnapshot,
|
||||
} from './router_state';
|
||||
import {PRIMARY_OUTLET} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
import {getOutlet, sortByMatchingOutlets} from './utils/config';
|
||||
import {isImmediateMatch, match, matchWithChecks, noLeftoversInUrl, split} from './utils/config_matching';
|
||||
import {
|
||||
isImmediateMatch,
|
||||
match,
|
||||
matchWithChecks,
|
||||
noLeftoversInUrl,
|
||||
split,
|
||||
} from './utils/config_matching';
|
||||
import {TreeNode} from './utils/tree';
|
||||
import {isEmptyError} from './utils/type_guards';
|
||||
|
||||
|
|
@ -32,15 +54,23 @@ import {isEmptyError} from './utils/type_guards';
|
|||
class NoLeftoversInUrl {}
|
||||
|
||||
export function recognize(
|
||||
injector: EnvironmentInjector, configLoader: RouterConfigLoader,
|
||||
rootComponentType: Type<any>|null, config: Routes, urlTree: UrlTree,
|
||||
urlSerializer: UrlSerializer,
|
||||
paramsInheritanceStrategy: ParamsInheritanceStrategy =
|
||||
'emptyOnly'): Observable<{state: RouterStateSnapshot, tree: UrlTree}> {
|
||||
injector: EnvironmentInjector,
|
||||
configLoader: RouterConfigLoader,
|
||||
rootComponentType: Type<any> | null,
|
||||
config: Routes,
|
||||
urlTree: UrlTree,
|
||||
urlSerializer: UrlSerializer,
|
||||
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly',
|
||||
): Observable<{state: RouterStateSnapshot; tree: UrlTree}> {
|
||||
return new Recognizer(
|
||||
injector, configLoader, rootComponentType, config, urlTree, paramsInheritanceStrategy,
|
||||
urlSerializer)
|
||||
.recognize();
|
||||
injector,
|
||||
configLoader,
|
||||
rootComponentType,
|
||||
config,
|
||||
urlTree,
|
||||
paramsInheritanceStrategy,
|
||||
urlSerializer,
|
||||
).recognize();
|
||||
}
|
||||
|
||||
const MAX_ALLOWED_REDIRECTS = 31;
|
||||
|
|
@ -51,80 +81,115 @@ export class Recognizer {
|
|||
allowRedirects = true;
|
||||
|
||||
constructor(
|
||||
private injector: EnvironmentInjector, private configLoader: RouterConfigLoader,
|
||||
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
|
||||
private paramsInheritanceStrategy: ParamsInheritanceStrategy,
|
||||
private readonly urlSerializer: UrlSerializer) {}
|
||||
private injector: EnvironmentInjector,
|
||||
private configLoader: RouterConfigLoader,
|
||||
private rootComponentType: Type<any> | null,
|
||||
private config: Routes,
|
||||
private urlTree: UrlTree,
|
||||
private paramsInheritanceStrategy: ParamsInheritanceStrategy,
|
||||
private readonly urlSerializer: UrlSerializer,
|
||||
) {}
|
||||
|
||||
private noMatchError(e: NoMatch): RuntimeError<RuntimeErrorCode.NO_MATCH> {
|
||||
return new RuntimeError(
|
||||
RuntimeErrorCode.NO_MATCH,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ?
|
||||
`Cannot match any routes. URL Segment: '${e.segmentGroup}'` :
|
||||
`'${e.segmentGroup}'`);
|
||||
RuntimeErrorCode.NO_MATCH,
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? `Cannot match any routes. URL Segment: '${e.segmentGroup}'`
|
||||
: `'${e.segmentGroup}'`,
|
||||
);
|
||||
}
|
||||
|
||||
recognize(): Observable<{state: RouterStateSnapshot, tree: UrlTree}> {
|
||||
recognize(): Observable<{state: RouterStateSnapshot; tree: UrlTree}> {
|
||||
const rootSegmentGroup = split(this.urlTree.root, [], [], this.config).segmentGroup;
|
||||
|
||||
return this.match(rootSegmentGroup).pipe(map(children => {
|
||||
// Use Object.freeze to prevent readers of the Router state from modifying it outside
|
||||
// of a navigation, resulting in the router being out of sync with the browser.
|
||||
const root = new ActivatedRouteSnapshot(
|
||||
[], Object.freeze({}), Object.freeze({...this.urlTree.queryParams}),
|
||||
this.urlTree.fragment, {}, PRIMARY_OUTLET, this.rootComponentType, null, {});
|
||||
return this.match(rootSegmentGroup).pipe(
|
||||
map((children) => {
|
||||
// Use Object.freeze to prevent readers of the Router state from modifying it outside
|
||||
// of a navigation, resulting in the router being out of sync with the browser.
|
||||
const root = new ActivatedRouteSnapshot(
|
||||
[],
|
||||
Object.freeze({}),
|
||||
Object.freeze({...this.urlTree.queryParams}),
|
||||
this.urlTree.fragment,
|
||||
{},
|
||||
PRIMARY_OUTLET,
|
||||
this.rootComponentType,
|
||||
null,
|
||||
{},
|
||||
);
|
||||
|
||||
const rootNode = new TreeNode(root, children);
|
||||
const routeState = new RouterStateSnapshot('', rootNode);
|
||||
const tree =
|
||||
createUrlTreeFromSnapshot(root, [], this.urlTree.queryParams, this.urlTree.fragment);
|
||||
// https://github.com/angular/angular/issues/47307
|
||||
// Creating the tree stringifies the query params
|
||||
// We don't want to do this here so reassign them to the original.
|
||||
tree.queryParams = this.urlTree.queryParams;
|
||||
routeState.url = this.urlSerializer.serialize(tree);
|
||||
this.inheritParamsAndData(routeState._root, null);
|
||||
return {state: routeState, tree};
|
||||
}));
|
||||
const rootNode = new TreeNode(root, children);
|
||||
const routeState = new RouterStateSnapshot('', rootNode);
|
||||
const tree = createUrlTreeFromSnapshot(
|
||||
root,
|
||||
[],
|
||||
this.urlTree.queryParams,
|
||||
this.urlTree.fragment,
|
||||
);
|
||||
// https://github.com/angular/angular/issues/47307
|
||||
// Creating the tree stringifies the query params
|
||||
// We don't want to do this here so reassign them to the original.
|
||||
tree.queryParams = this.urlTree.queryParams;
|
||||
routeState.url = this.urlSerializer.serialize(tree);
|
||||
this.inheritParamsAndData(routeState._root, null);
|
||||
return {state: routeState, tree};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private match(rootSegmentGroup: UrlSegmentGroup): Observable<TreeNode<ActivatedRouteSnapshot>[]> {
|
||||
const expanded$ =
|
||||
this.processSegmentGroup(this.injector, this.config, rootSegmentGroup, PRIMARY_OUTLET);
|
||||
return expanded$.pipe(catchError((e: any) => {
|
||||
if (e instanceof AbsoluteRedirect) {
|
||||
this.urlTree = e.urlTree;
|
||||
return this.match(e.urlTree.root);
|
||||
}
|
||||
if (e instanceof NoMatch) {
|
||||
throw this.noMatchError(e);
|
||||
}
|
||||
const expanded$ = this.processSegmentGroup(
|
||||
this.injector,
|
||||
this.config,
|
||||
rootSegmentGroup,
|
||||
PRIMARY_OUTLET,
|
||||
);
|
||||
return expanded$.pipe(
|
||||
catchError((e: any) => {
|
||||
if (e instanceof AbsoluteRedirect) {
|
||||
this.urlTree = e.urlTree;
|
||||
return this.match(e.urlTree.root);
|
||||
}
|
||||
if (e instanceof NoMatch) {
|
||||
throw this.noMatchError(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}));
|
||||
throw e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
inheritParamsAndData(
|
||||
routeNode: TreeNode<ActivatedRouteSnapshot>, parent: ActivatedRouteSnapshot|null): void {
|
||||
routeNode: TreeNode<ActivatedRouteSnapshot>,
|
||||
parent: ActivatedRouteSnapshot | null,
|
||||
): void {
|
||||
const route = routeNode.value;
|
||||
const i = getInherited(route, parent, this.paramsInheritanceStrategy);
|
||||
|
||||
route.params = Object.freeze(i.params);
|
||||
route.data = Object.freeze(i.data);
|
||||
|
||||
routeNode.children.forEach(n => this.inheritParamsAndData(n, route));
|
||||
routeNode.children.forEach((n) => this.inheritParamsAndData(n, route));
|
||||
}
|
||||
|
||||
processSegmentGroup(
|
||||
injector: EnvironmentInjector, config: Route[], segmentGroup: UrlSegmentGroup,
|
||||
outlet: string): Observable<TreeNode<ActivatedRouteSnapshot>[]> {
|
||||
injector: EnvironmentInjector,
|
||||
config: Route[],
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
outlet: string,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot>[]> {
|
||||
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return this.processChildren(injector, config, segmentGroup);
|
||||
}
|
||||
|
||||
return this.processSegment(injector, config, segmentGroup, segmentGroup.segments, outlet, true)
|
||||
.pipe(map(child => child instanceof TreeNode ? [child] : []));
|
||||
return this.processSegment(
|
||||
injector,
|
||||
config,
|
||||
segmentGroup,
|
||||
segmentGroup.segments,
|
||||
outlet,
|
||||
true,
|
||||
).pipe(map((child) => (child instanceof TreeNode ? [child] : [])));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -135,8 +200,11 @@ export class Recognizer {
|
|||
* @param segmentGroup - The `UrlSegmentGroup` whose children need to be matched against the
|
||||
* config.
|
||||
*/
|
||||
processChildren(injector: EnvironmentInjector, config: Route[], segmentGroup: UrlSegmentGroup):
|
||||
Observable<TreeNode<ActivatedRouteSnapshot>[]> {
|
||||
processChildren(
|
||||
injector: EnvironmentInjector,
|
||||
config: Route[],
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot>[]> {
|
||||
// Expand outlets one at a time, starting with the primary outlet. We need to do it this way
|
||||
// because an absolute redirect from the primary outlet takes precedence.
|
||||
const childOutlets: string[] = [];
|
||||
|
|
@ -147,71 +215,87 @@ export class Recognizer {
|
|||
childOutlets.push(child);
|
||||
}
|
||||
}
|
||||
return from(childOutlets)
|
||||
.pipe(
|
||||
concatMap(childOutlet => {
|
||||
const child = segmentGroup.children[childOutlet];
|
||||
// Sort the config so that routes with outlets that match the one being activated
|
||||
// appear first, followed by routes for other outlets, which might match if they have
|
||||
// an empty path.
|
||||
const sortedConfig = sortByMatchingOutlets(config, childOutlet);
|
||||
return this.processSegmentGroup(injector, sortedConfig, child, childOutlet);
|
||||
}),
|
||||
scan((children, outletChildren) => {
|
||||
children.push(...outletChildren);
|
||||
return children;
|
||||
}),
|
||||
defaultIfEmpty(null as TreeNode<ActivatedRouteSnapshot>[] | null),
|
||||
last(),
|
||||
mergeMap(children => {
|
||||
if (children === null) return noMatch(segmentGroup);
|
||||
// Because we may have matched two outlets to the same empty path segment, we can have
|
||||
// multiple activated results for the same outlet. We should merge the children of
|
||||
// these results so the final return value is only one `TreeNode` per outlet.
|
||||
const mergedChildren = mergeEmptyPathMatches(children);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// This should really never happen - we are only taking the first match for each
|
||||
// outlet and merge the empty path matches.
|
||||
checkOutletNameUniqueness(mergedChildren);
|
||||
}
|
||||
sortActivatedRouteSnapshots(mergedChildren);
|
||||
return of(mergedChildren);
|
||||
}),
|
||||
);
|
||||
return from(childOutlets).pipe(
|
||||
concatMap((childOutlet) => {
|
||||
const child = segmentGroup.children[childOutlet];
|
||||
// Sort the config so that routes with outlets that match the one being activated
|
||||
// appear first, followed by routes for other outlets, which might match if they have
|
||||
// an empty path.
|
||||
const sortedConfig = sortByMatchingOutlets(config, childOutlet);
|
||||
return this.processSegmentGroup(injector, sortedConfig, child, childOutlet);
|
||||
}),
|
||||
scan((children, outletChildren) => {
|
||||
children.push(...outletChildren);
|
||||
return children;
|
||||
}),
|
||||
defaultIfEmpty(null as TreeNode<ActivatedRouteSnapshot>[] | null),
|
||||
last(),
|
||||
mergeMap((children) => {
|
||||
if (children === null) return noMatch(segmentGroup);
|
||||
// Because we may have matched two outlets to the same empty path segment, we can have
|
||||
// multiple activated results for the same outlet. We should merge the children of
|
||||
// these results so the final return value is only one `TreeNode` per outlet.
|
||||
const mergedChildren = mergeEmptyPathMatches(children);
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
// This should really never happen - we are only taking the first match for each
|
||||
// outlet and merge the empty path matches.
|
||||
checkOutletNameUniqueness(mergedChildren);
|
||||
}
|
||||
sortActivatedRouteSnapshots(mergedChildren);
|
||||
return of(mergedChildren);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
processSegment(
|
||||
injector: EnvironmentInjector, routes: Route[], segmentGroup: UrlSegmentGroup,
|
||||
segments: UrlSegment[], outlet: string,
|
||||
allowRedirects: boolean): Observable<TreeNode<ActivatedRouteSnapshot>|NoLeftoversInUrl> {
|
||||
injector: EnvironmentInjector,
|
||||
routes: Route[],
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
allowRedirects: boolean,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot> | NoLeftoversInUrl> {
|
||||
return from(routes).pipe(
|
||||
concatMap(r => {
|
||||
return this
|
||||
.processSegmentAgainstRoute(
|
||||
r._injector ?? injector, routes, r, segmentGroup, segments, outlet,
|
||||
allowRedirects)
|
||||
.pipe(catchError((e: any) => {
|
||||
if (e instanceof NoMatch) {
|
||||
return of(null);
|
||||
}
|
||||
throw e;
|
||||
}));
|
||||
}),
|
||||
first((x): x is TreeNode<ActivatedRouteSnapshot>|NoLeftoversInUrl => !!x), catchError(e => {
|
||||
if (isEmptyError(e)) {
|
||||
if (noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return of(new NoLeftoversInUrl());
|
||||
concatMap((r) => {
|
||||
return this.processSegmentAgainstRoute(
|
||||
r._injector ?? injector,
|
||||
routes,
|
||||
r,
|
||||
segmentGroup,
|
||||
segments,
|
||||
outlet,
|
||||
allowRedirects,
|
||||
).pipe(
|
||||
catchError((e: any) => {
|
||||
if (e instanceof NoMatch) {
|
||||
return of(null);
|
||||
}
|
||||
return noMatch(segmentGroup);
|
||||
throw e;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
first((x): x is TreeNode<ActivatedRouteSnapshot> | NoLeftoversInUrl => !!x),
|
||||
catchError((e) => {
|
||||
if (isEmptyError(e)) {
|
||||
if (noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return of(new NoLeftoversInUrl());
|
||||
}
|
||||
throw e;
|
||||
}));
|
||||
return noMatch(segmentGroup);
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
processSegmentAgainstRoute(
|
||||
injector: EnvironmentInjector, routes: Route[], route: Route, rawSegment: UrlSegmentGroup,
|
||||
segments: UrlSegment[], outlet: string,
|
||||
allowRedirects: boolean): Observable<TreeNode<ActivatedRouteSnapshot>|NoLeftoversInUrl> {
|
||||
injector: EnvironmentInjector,
|
||||
routes: Route[],
|
||||
route: Route,
|
||||
rawSegment: UrlSegmentGroup,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
allowRedirects: boolean,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot> | NoLeftoversInUrl> {
|
||||
if (!isImmediateMatch(route, rawSegment, segments, outlet)) return noMatch(rawSegment);
|
||||
|
||||
if (route.redirectTo === undefined) {
|
||||
|
|
@ -220,22 +304,31 @@ export class Recognizer {
|
|||
|
||||
if (this.allowRedirects && allowRedirects) {
|
||||
return this.expandSegmentAgainstRouteUsingRedirect(
|
||||
injector, rawSegment, routes, route, segments, outlet);
|
||||
injector,
|
||||
rawSegment,
|
||||
routes,
|
||||
route,
|
||||
segments,
|
||||
outlet,
|
||||
);
|
||||
}
|
||||
|
||||
return noMatch(rawSegment);
|
||||
}
|
||||
|
||||
private expandSegmentAgainstRouteUsingRedirect(
|
||||
injector: EnvironmentInjector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
|
||||
segments: UrlSegment[],
|
||||
outlet: string): Observable<TreeNode<ActivatedRouteSnapshot>|NoLeftoversInUrl> {
|
||||
const {
|
||||
matched,
|
||||
consumedSegments,
|
||||
positionalParamSegments,
|
||||
remainingSegments,
|
||||
} = match(segmentGroup, route, segments);
|
||||
injector: EnvironmentInjector,
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
routes: Route[],
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot> | NoLeftoversInUrl> {
|
||||
const {matched, consumedSegments, positionalParamSegments, remainingSegments} = match(
|
||||
segmentGroup,
|
||||
route,
|
||||
segments,
|
||||
);
|
||||
if (!matched) return noMatch(segmentGroup);
|
||||
|
||||
// TODO(atscott): Move all of this under an if(ngDevMode) as a breaking change and allow stack
|
||||
|
|
@ -245,28 +338,42 @@ export class Recognizer {
|
|||
if (this.absoluteRedirectCount > MAX_ALLOWED_REDIRECTS) {
|
||||
if (ngDevMode) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INFINITE_REDIRECT,
|
||||
`Detected possible infinite redirect when redirecting from '${this.urlTree}' to '${
|
||||
route.redirectTo}'.\n` +
|
||||
`This is currently a dev mode only error but will become a` +
|
||||
` call stack size exceeded error in production in a future major version.`);
|
||||
RuntimeErrorCode.INFINITE_REDIRECT,
|
||||
`Detected possible infinite redirect when redirecting from '${this.urlTree}' to '${route.redirectTo}'.\n` +
|
||||
`This is currently a dev mode only error but will become a` +
|
||||
` call stack size exceeded error in production in a future major version.`,
|
||||
);
|
||||
}
|
||||
this.allowRedirects = false;
|
||||
}
|
||||
}
|
||||
const newTree = this.applyRedirects.applyRedirectCommands(
|
||||
consumedSegments, route.redirectTo!, positionalParamSegments);
|
||||
consumedSegments,
|
||||
route.redirectTo!,
|
||||
positionalParamSegments,
|
||||
);
|
||||
|
||||
return this.applyRedirects.lineralizeSegments(route, newTree)
|
||||
.pipe(mergeMap((newSegments: UrlSegment[]) => {
|
||||
return this.processSegment(
|
||||
injector, routes, segmentGroup, newSegments.concat(remainingSegments), outlet, false);
|
||||
}));
|
||||
return this.applyRedirects.lineralizeSegments(route, newTree).pipe(
|
||||
mergeMap((newSegments: UrlSegment[]) => {
|
||||
return this.processSegment(
|
||||
injector,
|
||||
routes,
|
||||
segmentGroup,
|
||||
newSegments.concat(remainingSegments),
|
||||
outlet,
|
||||
false,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
matchSegmentAgainstRoute(
|
||||
injector: EnvironmentInjector, rawSegment: UrlSegmentGroup, route: Route,
|
||||
segments: UrlSegment[], outlet: string): Observable<TreeNode<ActivatedRouteSnapshot>> {
|
||||
injector: EnvironmentInjector,
|
||||
rawSegment: UrlSegmentGroup,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
): Observable<TreeNode<ActivatedRouteSnapshot>> {
|
||||
const matchResult = matchWithChecks(rawSegment, route, segments, injector, this.urlSerializer);
|
||||
if (route.path === '**') {
|
||||
// Prior versions of the route matching algorithm would stop matching at the wildcard route.
|
||||
|
|
@ -276,34 +383,47 @@ export class Recognizer {
|
|||
rawSegment.children = {};
|
||||
}
|
||||
|
||||
return matchResult.pipe(switchMap((result) => {
|
||||
if (!result.matched) {
|
||||
return noMatch(rawSegment);
|
||||
}
|
||||
return matchResult.pipe(
|
||||
switchMap((result) => {
|
||||
if (!result.matched) {
|
||||
return noMatch(rawSegment);
|
||||
}
|
||||
|
||||
// If the route has an injector created from providers, we should start using that.
|
||||
injector = route._injector ?? injector;
|
||||
return this.getChildConfig(injector, route, segments)
|
||||
.pipe(switchMap(({routes: childConfig}) => {
|
||||
// If the route has an injector created from providers, we should start using that.
|
||||
injector = route._injector ?? injector;
|
||||
return this.getChildConfig(injector, route, segments).pipe(
|
||||
switchMap(({routes: childConfig}) => {
|
||||
const childInjector = route._loadedInjector ?? injector;
|
||||
|
||||
const {consumedSegments, remainingSegments, parameters} = result;
|
||||
const snapshot = new ActivatedRouteSnapshot(
|
||||
consumedSegments, parameters, Object.freeze({...this.urlTree.queryParams}),
|
||||
this.urlTree.fragment, getData(route), getOutlet(route),
|
||||
route.component ?? route._loadedComponent ?? null, route, getResolve(route));
|
||||
consumedSegments,
|
||||
parameters,
|
||||
Object.freeze({...this.urlTree.queryParams}),
|
||||
this.urlTree.fragment,
|
||||
getData(route),
|
||||
getOutlet(route),
|
||||
route.component ?? route._loadedComponent ?? null,
|
||||
route,
|
||||
getResolve(route),
|
||||
);
|
||||
|
||||
const {segmentGroup, slicedSegments} =
|
||||
split(rawSegment, consumedSegments, remainingSegments, childConfig);
|
||||
const {segmentGroup, slicedSegments} = split(
|
||||
rawSegment,
|
||||
consumedSegments,
|
||||
remainingSegments,
|
||||
childConfig,
|
||||
);
|
||||
|
||||
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return this.processChildren(childInjector, childConfig, segmentGroup)
|
||||
.pipe(map(children => {
|
||||
if (children === null) {
|
||||
return null;
|
||||
}
|
||||
return new TreeNode(snapshot, children);
|
||||
}));
|
||||
return this.processChildren(childInjector, childConfig, segmentGroup).pipe(
|
||||
map((children) => {
|
||||
if (children === null) {
|
||||
return null;
|
||||
}
|
||||
return new TreeNode(snapshot, children);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (childConfig.length === 0 && slicedSegments.length === 0) {
|
||||
|
|
@ -319,18 +439,28 @@ export class Recognizer {
|
|||
// {path: 'c', component: C},
|
||||
// ]}
|
||||
// Notice that the children of the named outlet are configured with the primary outlet
|
||||
return this
|
||||
.processSegment(
|
||||
childInjector, childConfig, segmentGroup, slicedSegments,
|
||||
matchedOnOutlet ? PRIMARY_OUTLET : outlet, true)
|
||||
.pipe(map(child => {
|
||||
return new TreeNode(snapshot, child instanceof TreeNode ? [child] : []);
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
return this.processSegment(
|
||||
childInjector,
|
||||
childConfig,
|
||||
segmentGroup,
|
||||
slicedSegments,
|
||||
matchedOnOutlet ? PRIMARY_OUTLET : outlet,
|
||||
true,
|
||||
).pipe(
|
||||
map((child) => {
|
||||
return new TreeNode(snapshot, child instanceof TreeNode ? [child] : []);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
private getChildConfig(injector: EnvironmentInjector, route: Route, segments: UrlSegment[]):
|
||||
Observable<LoadedRouterConfig> {
|
||||
private getChildConfig(
|
||||
injector: EnvironmentInjector,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
): Observable<LoadedRouterConfig> {
|
||||
if (route.children) {
|
||||
// The children belong to the same module
|
||||
return of({routes: route.children, injector});
|
||||
|
|
@ -342,17 +472,19 @@ export class Recognizer {
|
|||
return of({routes: route._loadedRoutes, injector: route._loadedInjector});
|
||||
}
|
||||
|
||||
return runCanLoadGuards(injector, route, segments, this.urlSerializer)
|
||||
.pipe(mergeMap((shouldLoadResult: boolean) => {
|
||||
if (shouldLoadResult) {
|
||||
return this.configLoader.loadChildren(injector, route)
|
||||
.pipe(tap((cfg: LoadedRouterConfig) => {
|
||||
route._loadedRoutes = cfg.routes;
|
||||
route._loadedInjector = cfg.injector;
|
||||
}));
|
||||
}
|
||||
return canLoadFails(route);
|
||||
}));
|
||||
return runCanLoadGuards(injector, route, segments, this.urlSerializer).pipe(
|
||||
mergeMap((shouldLoadResult: boolean) => {
|
||||
if (shouldLoadResult) {
|
||||
return this.configLoader.loadChildren(injector, route).pipe(
|
||||
tap((cfg: LoadedRouterConfig) => {
|
||||
route._loadedRoutes = cfg.routes;
|
||||
route._loadedInjector = cfg.injector;
|
||||
}),
|
||||
);
|
||||
}
|
||||
return canLoadFails(route);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return of({routes: [], injector});
|
||||
|
|
@ -377,8 +509,9 @@ function hasEmptyPathConfig(node: TreeNode<ActivatedRouteSnapshot>) {
|
|||
* the children from each duplicate. This is necessary because different outlets can match a
|
||||
* single empty path route config and the results need to then be merged.
|
||||
*/
|
||||
function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
|
||||
Array<TreeNode<ActivatedRouteSnapshot>> {
|
||||
function mergeEmptyPathMatches(
|
||||
nodes: Array<TreeNode<ActivatedRouteSnapshot>>,
|
||||
): Array<TreeNode<ActivatedRouteSnapshot>> {
|
||||
const result: Array<TreeNode<ActivatedRouteSnapshot>> = [];
|
||||
// The set of nodes which contain children that were merged from two duplicate empty path nodes.
|
||||
const mergedNodes: Set<TreeNode<ActivatedRouteSnapshot>> = new Set();
|
||||
|
|
@ -389,8 +522,9 @@ function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
|
|||
continue;
|
||||
}
|
||||
|
||||
const duplicateEmptyPathNode =
|
||||
result.find(resultNode => node.value.routeConfig === resultNode.value.routeConfig);
|
||||
const duplicateEmptyPathNode = result.find(
|
||||
(resultNode) => node.value.routeConfig === resultNode.value.routeConfig,
|
||||
);
|
||||
if (duplicateEmptyPathNode !== undefined) {
|
||||
duplicateEmptyPathNode.children.push(...node.children);
|
||||
mergedNodes.add(duplicateEmptyPathNode);
|
||||
|
|
@ -406,20 +540,21 @@ function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
|
|||
const mergedChildren = mergeEmptyPathMatches(mergedNode.children);
|
||||
result.push(new TreeNode(mergedNode.value, mergedChildren));
|
||||
}
|
||||
return result.filter(n => !mergedNodes.has(n));
|
||||
return result.filter((n) => !mergedNodes.has(n));
|
||||
}
|
||||
|
||||
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
||||
const names: {[k: string]: ActivatedRouteSnapshot} = {};
|
||||
nodes.forEach(n => {
|
||||
nodes.forEach((n) => {
|
||||
const routeWithSameOutletName = names[n.value.outlet];
|
||||
if (routeWithSameOutletName) {
|
||||
const p = routeWithSameOutletName.url.map(s => s.toString()).join('/');
|
||||
const c = n.value.url.map(s => s.toString()).join('/');
|
||||
const p = routeWithSameOutletName.url.map((s) => s.toString()).join('/');
|
||||
const c = n.value.url.map((s) => s.toString()).join('/');
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.TWO_SEGMENTS_WITH_SAME_OUTLET,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Two segments cannot have the same outlet name: '${p}' and '${c}'.`);
|
||||
RuntimeErrorCode.TWO_SEGMENTS_WITH_SAME_OUTLET,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Two segments cannot have the same outlet name: '${p}' and '${c}'.`,
|
||||
);
|
||||
}
|
||||
names[n.value.outlet] = n.value;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ export type DetachedRouteHandle = {};
|
|||
|
||||
/** @internal */
|
||||
export type DetachedRouteHandleInternal = {
|
||||
contexts: Map<string, OutletContext>,
|
||||
componentRef: ComponentRef<any>,
|
||||
route: TreeNode<ActivatedRoute>,
|
||||
contexts: Map<string, OutletContext>;
|
||||
componentRef: ComponentRef<any>;
|
||||
route: TreeNode<ActivatedRoute>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -48,13 +48,13 @@ export abstract class RouteReuseStrategy {
|
|||
*
|
||||
* Storing a `null` value should erase the previously stored value.
|
||||
*/
|
||||
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle|null): void;
|
||||
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void;
|
||||
|
||||
/** Determines if this route (and its subtree) should be reattached */
|
||||
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||
|
||||
/** Retrieves the previously stored route */
|
||||
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null;
|
||||
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
|
||||
|
||||
/** Determines if a route should be reused */
|
||||
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||
|
|
@ -97,7 +97,7 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
|
|||
}
|
||||
|
||||
/** Returns `null` because this strategy does not store routes for later re-use. */
|
||||
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null {
|
||||
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -112,5 +112,4 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
|
|||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class DefaultRouteReuseStrategy extends BaseRouteReuseStrategy {
|
||||
}
|
||||
export class DefaultRouteReuseStrategy extends BaseRouteReuseStrategy {}
|
||||
|
|
|
|||
|
|
@ -7,27 +7,57 @@
|
|||
*/
|
||||
|
||||
import {Location} from '@angular/common';
|
||||
import {inject, Injectable, NgZone, Type, ɵConsole as Console, ɵPendingTasks as PendingTasks, ɵRuntimeError as RuntimeError} from '@angular/core';
|
||||
import {
|
||||
inject,
|
||||
Injectable,
|
||||
NgZone,
|
||||
Type,
|
||||
ɵConsole as Console,
|
||||
ɵPendingTasks as PendingTasks,
|
||||
ɵRuntimeError as RuntimeError,
|
||||
} from '@angular/core';
|
||||
import {Observable, Subject, Subscription, SubscriptionLike} from 'rxjs';
|
||||
|
||||
import {createSegmentGroupFromRoute, createUrlTreeFromSegmentGroup} from './create_url_tree';
|
||||
import {INPUT_BINDER} from './directives/router_outlet';
|
||||
import {RuntimeErrorCode} from './errors';
|
||||
import {BeforeActivateRoutes, Event, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationTrigger, PrivateRouterEvents, RedirectRequest} from './events';
|
||||
import {
|
||||
BeforeActivateRoutes,
|
||||
Event,
|
||||
IMPERATIVE_NAVIGATION,
|
||||
NavigationCancel,
|
||||
NavigationCancellationCode,
|
||||
NavigationEnd,
|
||||
NavigationTrigger,
|
||||
PrivateRouterEvents,
|
||||
RedirectRequest,
|
||||
} from './events';
|
||||
import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models';
|
||||
import {isBrowserTriggeredNavigation, Navigation, NavigationExtras, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
|
||||
import {
|
||||
isBrowserTriggeredNavigation,
|
||||
Navigation,
|
||||
NavigationExtras,
|
||||
NavigationTransitions,
|
||||
RestoredState,
|
||||
UrlCreationOptions,
|
||||
} from './navigation_transition';
|
||||
import {RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ROUTER_CONFIGURATION} from './router_config';
|
||||
import {ROUTES} from './router_config_loader';
|
||||
import {Params} from './shared';
|
||||
import {StateManager} from './statemanager/state_manager';
|
||||
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
import {containsTree, IsActiveMatchOptions, isUrlTree, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
import {
|
||||
containsTree,
|
||||
IsActiveMatchOptions,
|
||||
isUrlTree,
|
||||
UrlSegmentGroup,
|
||||
UrlSerializer,
|
||||
UrlTree,
|
||||
} from './url_tree';
|
||||
import {standardizeConfig, validateConfig} from './utils/config';
|
||||
import {afterNextNavigation} from './utils/navigations';
|
||||
|
||||
|
||||
|
||||
function defaultErrorHandler(error: any): never {
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -40,7 +70,7 @@ export const exactMatchOptions: IsActiveMatchOptions = {
|
|||
paths: 'exact',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
queryParams: 'exact'
|
||||
queryParams: 'exact',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -51,7 +81,7 @@ export const subsetMatchOptions: IsActiveMatchOptions = {
|
|||
paths: 'subset',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
queryParams: 'subset'
|
||||
queryParams: 'subset',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -160,26 +190,29 @@ export class Router {
|
|||
|
||||
this.resetConfig(this.config);
|
||||
|
||||
this.navigationTransitions.setupNavigations(this, this.currentUrlTree, this.routerState)
|
||||
.subscribe({
|
||||
error: (e) => {
|
||||
this.console.warn(ngDevMode ? `Unhandled Navigation Error: ${e}` : e);
|
||||
}
|
||||
});
|
||||
this.navigationTransitions
|
||||
.setupNavigations(this, this.currentUrlTree, this.routerState)
|
||||
.subscribe({
|
||||
error: (e) => {
|
||||
this.console.warn(ngDevMode ? `Unhandled Navigation Error: ${e}` : e);
|
||||
},
|
||||
});
|
||||
this.subscribeToNavigationEvents();
|
||||
}
|
||||
|
||||
|
||||
private eventsSubscription = new Subscription();
|
||||
private subscribeToNavigationEvents() {
|
||||
const subscription = this.navigationTransitions.events.subscribe(e => {
|
||||
const subscription = this.navigationTransitions.events.subscribe((e) => {
|
||||
try {
|
||||
const currentTransition = this.navigationTransitions.currentTransition;
|
||||
const currentNavigation = this.navigationTransitions.currentNavigation;
|
||||
if (currentTransition !== null && currentNavigation !== null) {
|
||||
this.stateManager.handleRouterEvent(e, currentNavigation);
|
||||
if (e instanceof NavigationCancel && e.code !== NavigationCancellationCode.Redirect &&
|
||||
e.code !== NavigationCancellationCode.SupersededByNewNavigation) {
|
||||
if (
|
||||
e instanceof NavigationCancel &&
|
||||
e.code !== NavigationCancellationCode.Redirect &&
|
||||
e.code !== NavigationCancellationCode.SupersededByNewNavigation
|
||||
) {
|
||||
// It seems weird that `navigated` is set to `true` when the navigation is rejected,
|
||||
// however it's how things were written initially. Investigation would need to be done
|
||||
// to determine if this can be removed.
|
||||
|
|
@ -187,8 +220,10 @@ export class Router {
|
|||
} else if (e instanceof NavigationEnd) {
|
||||
this.navigated = true;
|
||||
} else if (e instanceof RedirectRequest) {
|
||||
const mergedTree =
|
||||
this.urlHandlingStrategy.merge(e.url, currentTransition.currentRawUrl);
|
||||
const mergedTree = this.urlHandlingStrategy.merge(
|
||||
e.url,
|
||||
currentTransition.currentRawUrl,
|
||||
);
|
||||
const extras = {
|
||||
// Persist transient navigation info from the original navigation request.
|
||||
info: currentTransition.extras.info,
|
||||
|
|
@ -197,14 +232,15 @@ export class Router {
|
|||
// updates or if the navigation was triggered by the browser (back
|
||||
// button, URL bar, etc). We want to replace that item in history
|
||||
// if the navigation is rejected.
|
||||
replaceUrl: this.urlUpdateStrategy === 'eager' ||
|
||||
isBrowserTriggeredNavigation(currentTransition.source)
|
||||
replaceUrl:
|
||||
this.urlUpdateStrategy === 'eager' ||
|
||||
isBrowserTriggeredNavigation(currentTransition.source),
|
||||
};
|
||||
|
||||
this.scheduleNavigation(mergedTree, IMPERATIVE_NAVIGATION, null, extras, {
|
||||
resolve: currentTransition.resolve,
|
||||
reject: currentTransition.reject,
|
||||
promise: currentTransition.promise
|
||||
promise: currentTransition.promise,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -236,7 +272,10 @@ export class Router {
|
|||
this.setUpLocationChangeListener();
|
||||
if (!this.navigationTransitions.hasRequestedNavigation) {
|
||||
this.navigateToSyncWithBrowser(
|
||||
this.location.path(true), IMPERATIVE_NAVIGATION, this.stateManager.restoredState());
|
||||
this.location.path(true),
|
||||
IMPERATIVE_NAVIGATION,
|
||||
this.stateManager.restoredState(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,13 +289,13 @@ export class Router {
|
|||
// already patch onPopState, so location change callback will
|
||||
// run into ngZone
|
||||
this.nonRouterCurrentEntryChangeSubscription ??=
|
||||
this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => {
|
||||
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
|
||||
// hybrid apps.
|
||||
setTimeout(() => {
|
||||
this.navigateToSyncWithBrowser(url, 'popstate', state);
|
||||
}, 0);
|
||||
});
|
||||
this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => {
|
||||
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
|
||||
// hybrid apps.
|
||||
setTimeout(() => {
|
||||
this.navigateToSyncWithBrowser(url, 'popstate', state);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -267,7 +306,10 @@ export class Router {
|
|||
* the Router needs to respond to ensure its internal state matches.
|
||||
*/
|
||||
private navigateToSyncWithBrowser(
|
||||
url: string, source: NavigationTrigger, state: RestoredState|null|undefined) {
|
||||
url: string,
|
||||
source: NavigationTrigger,
|
||||
state: RestoredState | null | undefined,
|
||||
) {
|
||||
const extras: NavigationExtras = {replaceUrl: true};
|
||||
|
||||
// TODO: restoredState should always include the entire state, regardless
|
||||
|
|
@ -304,7 +346,7 @@ export class Router {
|
|||
* Returns the current `Navigation` object when the router is navigating,
|
||||
* and `null` when idle.
|
||||
*/
|
||||
getCurrentNavigation(): Navigation|null {
|
||||
getCurrentNavigation(): Navigation | null {
|
||||
return this.navigationTransitions.currentNavigation;
|
||||
}
|
||||
|
||||
|
|
@ -312,7 +354,7 @@ export class Router {
|
|||
* The `Navigation` object of the most recent navigation to succeed and `null` if there
|
||||
* has not been a successful navigation yet.
|
||||
*/
|
||||
get lastSuccessfulNavigation(): Navigation|null {
|
||||
get lastSuccessfulNavigation(): Navigation | null {
|
||||
return this.navigationTransitions.lastSuccessfulNavigation;
|
||||
}
|
||||
|
||||
|
|
@ -404,9 +446,9 @@ export class Router {
|
|||
*/
|
||||
createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree {
|
||||
const {relativeTo, queryParams, fragment, queryParamsHandling, preserveFragment} =
|
||||
navigationExtras;
|
||||
navigationExtras;
|
||||
const f = preserveFragment ? this.currentUrlTree.fragment : fragment;
|
||||
let q: Params|null = null;
|
||||
let q: Params | null = null;
|
||||
switch (queryParamsHandling) {
|
||||
case 'merge':
|
||||
q = {...this.currentUrlTree.queryParams, ...queryParams};
|
||||
|
|
@ -421,7 +463,7 @@ export class Router {
|
|||
q = this.removeEmptyProps(q);
|
||||
}
|
||||
|
||||
let relativeToUrlSegmentGroup: UrlSegmentGroup|undefined;
|
||||
let relativeToUrlSegmentGroup: UrlSegmentGroup | undefined;
|
||||
try {
|
||||
const relativeToSnapshot = relativeTo ? relativeTo.snapshot : this.routerState.snapshot.root;
|
||||
relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeToSnapshot);
|
||||
|
|
@ -470,13 +512,17 @@ export class Router {
|
|||
* @see [Routing and Navigation guide](guide/router)
|
||||
*
|
||||
*/
|
||||
navigateByUrl(url: string|UrlTree, extras: NavigationBehaviorOptions = {
|
||||
skipLocationChange: false
|
||||
}): Promise<boolean> {
|
||||
navigateByUrl(
|
||||
url: string | UrlTree,
|
||||
extras: NavigationBehaviorOptions = {
|
||||
skipLocationChange: false,
|
||||
},
|
||||
): Promise<boolean> {
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
if (this.isNgZoneEnabled && !NgZone.isInAngularZone()) {
|
||||
this.console.warn(
|
||||
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`);
|
||||
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,8 +562,10 @@ export class Router {
|
|||
* @see [Routing and Navigation guide](guide/router)
|
||||
*
|
||||
*/
|
||||
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
|
||||
Promise<boolean> {
|
||||
navigate(
|
||||
commands: any[],
|
||||
extras: NavigationExtras = {skipLocationChange: false},
|
||||
): Promise<boolean> {
|
||||
validateCommands(commands);
|
||||
return this.navigateByUrl(this.createUrlTree(commands, extras), extras);
|
||||
}
|
||||
|
|
@ -547,14 +595,14 @@ export class Router {
|
|||
* - The equivalent for `false` is
|
||||
* `{paths: 'subset', queryParams: 'subset', fragment: 'ignored', matrixParams: 'ignored'}`.
|
||||
*/
|
||||
isActive(url: string|UrlTree, exact: boolean): boolean;
|
||||
isActive(url: string | UrlTree, exact: boolean): boolean;
|
||||
/**
|
||||
* Returns whether the url is activated.
|
||||
*/
|
||||
isActive(url: string|UrlTree, matchOptions: IsActiveMatchOptions): boolean;
|
||||
isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean;
|
||||
/** @internal */
|
||||
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean;
|
||||
isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean {
|
||||
isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean;
|
||||
isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean {
|
||||
let options: IsActiveMatchOptions;
|
||||
if (matchOptions === true) {
|
||||
options = {...exactMatchOptions};
|
||||
|
|
@ -581,9 +629,12 @@ export class Router {
|
|||
}
|
||||
|
||||
private scheduleNavigation(
|
||||
rawUrl: UrlTree, source: NavigationTrigger, restoredState: RestoredState|null,
|
||||
extras: NavigationExtras,
|
||||
priorPromise?: {resolve: any, reject: any, promise: Promise<boolean>}): Promise<boolean> {
|
||||
rawUrl: UrlTree,
|
||||
source: NavigationTrigger,
|
||||
restoredState: RestoredState | null,
|
||||
extras: NavigationExtras,
|
||||
priorPromise?: {resolve: any; reject: any; promise: Promise<boolean>},
|
||||
): Promise<boolean> {
|
||||
if (this.disposed) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
|
@ -621,7 +672,7 @@ export class Router {
|
|||
reject,
|
||||
promise,
|
||||
currentSnapshot: this.routerState.snapshot,
|
||||
currentRouterState: this.routerState
|
||||
currentRouterState: this.routerState,
|
||||
});
|
||||
|
||||
// Make sure that the error is propagated even though `processNavigations` catch
|
||||
|
|
@ -637,13 +688,14 @@ function validateCommands(commands: string[]): void {
|
|||
const cmd = commands[i];
|
||||
if (cmd == null) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.NULLISH_COMMAND,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`The requested path contains ${cmd} segment at index ${i}`);
|
||||
RuntimeErrorCode.NULLISH_COMMAND,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`The requested path contains ${cmd} segment at index ${i}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPublicRouterEvent(e: Event|PrivateRouterEvents): e is Event {
|
||||
return (!(e instanceof BeforeActivateRoutes) && !(e instanceof RedirectRequest));
|
||||
function isPublicRouterEvent(e: Event | PrivateRouterEvents): e is Event {
|
||||
return !(e instanceof BeforeActivateRoutes) && !(e instanceof RedirectRequest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {InjectionToken} from '@angular/core';
|
|||
|
||||
import {OnSameUrlNavigation} from './models';
|
||||
|
||||
|
||||
/**
|
||||
* Error handler that is invoked when a navigation error occurs.
|
||||
*
|
||||
|
|
@ -46,7 +45,7 @@ export type ErrorHandler = (error: any) => any;
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export type InitialNavigation = 'disabled'|'enabledBlocking'|'enabledNonBlocking';
|
||||
export type InitialNavigation = 'disabled' | 'enabledBlocking' | 'enabledNonBlocking';
|
||||
|
||||
/**
|
||||
* Extra configuration options that can be used with the `withRouterConfig` function.
|
||||
|
|
@ -75,7 +74,7 @@ export interface RouterConfigOptions {
|
|||
*
|
||||
* The default value is `replace` when not set.
|
||||
*/
|
||||
canceledNavigationResolution?: 'replace'|'computed';
|
||||
canceledNavigationResolution?: 'replace' | 'computed';
|
||||
|
||||
/**
|
||||
* Configures the default for handling a navigation request to the current URL.
|
||||
|
|
@ -103,7 +102,7 @@ export interface RouterConfigOptions {
|
|||
* `a;foo=bar/b`.
|
||||
*
|
||||
*/
|
||||
paramsInheritanceStrategy?: 'emptyOnly'|'always';
|
||||
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
||||
|
||||
/**
|
||||
* Defines when the router updates the browser URL. By default ('deferred'),
|
||||
|
|
@ -112,7 +111,7 @@ export interface RouterConfigOptions {
|
|||
* Updating the URL early allows you to handle a failure of navigation by
|
||||
* showing an error message with the URL that failed.
|
||||
*/
|
||||
urlUpdateStrategy?: 'deferred'|'eager';
|
||||
urlUpdateStrategy?: 'deferred' | 'eager';
|
||||
|
||||
/**
|
||||
* When `true`, the `Promise` will instead resolve with `false`, as it does with other failed
|
||||
|
|
@ -138,7 +137,7 @@ export interface InMemoryScrollingOptions {
|
|||
* Anchor scrolling does not happen on 'popstate'. Instead, we restore the position
|
||||
* that we stored or scroll to the top.
|
||||
*/
|
||||
anchorScrolling?: 'disabled'|'enabled';
|
||||
anchorScrolling?: 'disabled' | 'enabled';
|
||||
|
||||
/**
|
||||
* Configures if the scroll position needs to be restored when navigating back.
|
||||
|
|
@ -174,7 +173,7 @@ export interface InMemoryScrollingOptions {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
scrollPositionRestoration?: 'disabled'|'enabled'|'top';
|
||||
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -255,7 +254,7 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
|
|||
* When given a function, the router invokes the function every time
|
||||
* it restores scroll position.
|
||||
*/
|
||||
scrollOffset?: [number, number]|(() => [number, number]);
|
||||
scrollOffset?: [number, number] | (() => [number, number]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -264,7 +263,9 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
|
|||
* @publicApi
|
||||
*/
|
||||
export const ROUTER_CONFIGURATION = new InjectionToken<ExtraOptions>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'router config' : '', {
|
||||
providedIn: 'root',
|
||||
factory: () => ({}),
|
||||
});
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'router config' : '',
|
||||
{
|
||||
providedIn: 'root',
|
||||
factory: () => ({}),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Compiler, EnvironmentInjector, inject, Injectable, InjectionToken, Injector, NgModuleFactory, Type} from '@angular/core';
|
||||
import {
|
||||
Compiler,
|
||||
EnvironmentInjector,
|
||||
inject,
|
||||
Injectable,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
NgModuleFactory,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import {ConnectableObservable, from, Observable, of, Subject} from 'rxjs';
|
||||
import {finalize, map, mergeMap, refCount, tap} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -14,8 +23,6 @@ import {DefaultExport, LoadedRouterConfig, Route, Routes} from './models';
|
|||
import {wrapIntoObservable} from './utils/collection';
|
||||
import {assertStandalone, standardizeConfig, validateConfig} from './utils/config';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The [DI token](guide/glossary/#di-token) for a router configuration.
|
||||
*
|
||||
|
|
@ -48,24 +55,24 @@ export class RouterConfigLoader {
|
|||
if (this.onLoadStartListener) {
|
||||
this.onLoadStartListener(route);
|
||||
}
|
||||
const loadRunner = wrapIntoObservable(route.loadComponent!())
|
||||
.pipe(
|
||||
map(maybeUnwrapDefaultExport),
|
||||
tap(component => {
|
||||
if (this.onLoadEndListener) {
|
||||
this.onLoadEndListener(route);
|
||||
}
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
assertStandalone(route.path ?? '', component);
|
||||
route._loadedComponent = component;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.componentLoaders.delete(route);
|
||||
}),
|
||||
);
|
||||
const loadRunner = wrapIntoObservable(route.loadComponent!()).pipe(
|
||||
map(maybeUnwrapDefaultExport),
|
||||
tap((component) => {
|
||||
if (this.onLoadEndListener) {
|
||||
this.onLoadEndListener(route);
|
||||
}
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
assertStandalone(route.path ?? '', component);
|
||||
route._loadedComponent = component;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.componentLoaders.delete(route);
|
||||
}),
|
||||
);
|
||||
// Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much
|
||||
const loader =
|
||||
new ConnectableObservable(loadRunner, () => new Subject<Type<unknown>>()).pipe(refCount());
|
||||
const loader = new ConnectableObservable(loadRunner, () => new Subject<Type<unknown>>()).pipe(
|
||||
refCount(),
|
||||
);
|
||||
this.componentLoaders.set(route, loader);
|
||||
return loader;
|
||||
}
|
||||
|
|
@ -80,16 +87,22 @@ export class RouterConfigLoader {
|
|||
if (this.onLoadStartListener) {
|
||||
this.onLoadStartListener(route);
|
||||
}
|
||||
const moduleFactoryOrRoutes$ =
|
||||
loadChildren(route, this.compiler, parentInjector, this.onLoadEndListener);
|
||||
const moduleFactoryOrRoutes$ = loadChildren(
|
||||
route,
|
||||
this.compiler,
|
||||
parentInjector,
|
||||
this.onLoadEndListener,
|
||||
);
|
||||
const loadRunner = moduleFactoryOrRoutes$.pipe(
|
||||
finalize(() => {
|
||||
this.childrenLoaders.delete(route);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.childrenLoaders.delete(route);
|
||||
}),
|
||||
);
|
||||
// Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much
|
||||
const loader = new ConnectableObservable(loadRunner, () => new Subject<LoadedRouterConfig>())
|
||||
.pipe(refCount());
|
||||
const loader = new ConnectableObservable(
|
||||
loadRunner,
|
||||
() => new Subject<LoadedRouterConfig>(),
|
||||
).pipe(refCount());
|
||||
this.childrenLoaders.set(route, loader);
|
||||
return loader;
|
||||
}
|
||||
|
|
@ -104,54 +117,56 @@ export class RouterConfigLoader {
|
|||
* an update to the extractor.
|
||||
*/
|
||||
export function loadChildren(
|
||||
route: Route, compiler: Compiler, parentInjector: Injector,
|
||||
onLoadEndListener?: (r: Route) => void): Observable<LoadedRouterConfig> {
|
||||
return wrapIntoObservable(route.loadChildren!())
|
||||
.pipe(
|
||||
map(maybeUnwrapDefaultExport),
|
||||
mergeMap((t) => {
|
||||
if (t instanceof NgModuleFactory || Array.isArray(t)) {
|
||||
return of(t);
|
||||
} else {
|
||||
return from(compiler.compileModuleAsync(t));
|
||||
}
|
||||
}),
|
||||
map((factoryOrRoutes: NgModuleFactory<any>|Routes) => {
|
||||
if (onLoadEndListener) {
|
||||
onLoadEndListener(route);
|
||||
}
|
||||
// This injector comes from the `NgModuleRef` when lazy loading an `NgModule`. There is
|
||||
// no injector associated with lazy loading a `Route` array.
|
||||
let injector: EnvironmentInjector|undefined;
|
||||
let rawRoutes: Route[];
|
||||
let requireStandaloneComponents = false;
|
||||
if (Array.isArray(factoryOrRoutes)) {
|
||||
rawRoutes = factoryOrRoutes;
|
||||
requireStandaloneComponents = true;
|
||||
} else {
|
||||
injector = factoryOrRoutes.create(parentInjector).injector;
|
||||
// When loading a module that doesn't provide `RouterModule.forChild()` preloader
|
||||
// will get stuck in an infinite loop. The child module's Injector will look to
|
||||
// its parent `Injector` when it doesn't find any ROUTES so it will return routes
|
||||
// for it's parent module instead.
|
||||
rawRoutes = injector.get(ROUTES, [], {optional: true, self: true}).flat();
|
||||
}
|
||||
const routes = rawRoutes.map(standardizeConfig);
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
validateConfig(routes, route.path, requireStandaloneComponents);
|
||||
return {routes, injector};
|
||||
}),
|
||||
);
|
||||
route: Route,
|
||||
compiler: Compiler,
|
||||
parentInjector: Injector,
|
||||
onLoadEndListener?: (r: Route) => void,
|
||||
): Observable<LoadedRouterConfig> {
|
||||
return wrapIntoObservable(route.loadChildren!()).pipe(
|
||||
map(maybeUnwrapDefaultExport),
|
||||
mergeMap((t) => {
|
||||
if (t instanceof NgModuleFactory || Array.isArray(t)) {
|
||||
return of(t);
|
||||
} else {
|
||||
return from(compiler.compileModuleAsync(t));
|
||||
}
|
||||
}),
|
||||
map((factoryOrRoutes: NgModuleFactory<any> | Routes) => {
|
||||
if (onLoadEndListener) {
|
||||
onLoadEndListener(route);
|
||||
}
|
||||
// This injector comes from the `NgModuleRef` when lazy loading an `NgModule`. There is
|
||||
// no injector associated with lazy loading a `Route` array.
|
||||
let injector: EnvironmentInjector | undefined;
|
||||
let rawRoutes: Route[];
|
||||
let requireStandaloneComponents = false;
|
||||
if (Array.isArray(factoryOrRoutes)) {
|
||||
rawRoutes = factoryOrRoutes;
|
||||
requireStandaloneComponents = true;
|
||||
} else {
|
||||
injector = factoryOrRoutes.create(parentInjector).injector;
|
||||
// When loading a module that doesn't provide `RouterModule.forChild()` preloader
|
||||
// will get stuck in an infinite loop. The child module's Injector will look to
|
||||
// its parent `Injector` when it doesn't find any ROUTES so it will return routes
|
||||
// for it's parent module instead.
|
||||
rawRoutes = injector.get(ROUTES, [], {optional: true, self: true}).flat();
|
||||
}
|
||||
const routes = rawRoutes.map(standardizeConfig);
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
validateConfig(routes, route.path, requireStandaloneComponents);
|
||||
return {routes, injector};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function isWrappedDefaultExport<T>(value: T|DefaultExport<T>): value is DefaultExport<T> {
|
||||
function isWrappedDefaultExport<T>(value: T | DefaultExport<T>): value is DefaultExport<T> {
|
||||
// We use `in` here with a string key `'default'`, because we expect `DefaultExport` objects to be
|
||||
// dynamically imported ES modules with a spec-mandated `default` key. Thus we don't expect that
|
||||
// `default` will be a renamed property.
|
||||
return value && typeof value === 'object' && 'default' in value;
|
||||
}
|
||||
|
||||
function maybeUnwrapDefaultExport<T>(input: T|DefaultExport<T>): T {
|
||||
function maybeUnwrapDefaultExport<T>(input: T | DefaultExport<T>): T {
|
||||
// As per `isWrappedDefaultExport`, the `default` key here is generated by the browser and not
|
||||
// subject to property renaming, so we reference it with bracket access.
|
||||
return isWrappedDefaultExport(input) ? input['default'] : input;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,27 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, ViewportScroller} from '@angular/common';
|
||||
import {APP_BOOTSTRAP_LISTENER, ComponentRef, inject, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, Provider, SkipSelf, ɵRuntimeError as RuntimeError} from '@angular/core';
|
||||
import {
|
||||
HashLocationStrategy,
|
||||
Location,
|
||||
LocationStrategy,
|
||||
PathLocationStrategy,
|
||||
ViewportScroller,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
APP_BOOTSTRAP_LISTENER,
|
||||
ComponentRef,
|
||||
inject,
|
||||
Inject,
|
||||
InjectionToken,
|
||||
ModuleWithProviders,
|
||||
NgModule,
|
||||
NgZone,
|
||||
Optional,
|
||||
Provider,
|
||||
SkipSelf,
|
||||
ɵRuntimeError as RuntimeError,
|
||||
} from '@angular/core';
|
||||
|
||||
import {EmptyOutletComponent} from './components/empty_outlet';
|
||||
import {RouterLink} from './directives/router_link';
|
||||
|
|
@ -16,7 +35,17 @@ import {RouterOutlet} from './directives/router_outlet';
|
|||
import {RuntimeErrorCode} from './errors';
|
||||
import {Routes} from './models';
|
||||
import {NavigationTransitions} from './navigation_transition';
|
||||
import {getBootstrapListener, rootRoute, ROUTER_IS_PROVIDED, withComponentInputBinding, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withPreloading, withViewTransitions} from './provide_router';
|
||||
import {
|
||||
getBootstrapListener,
|
||||
rootRoute,
|
||||
ROUTER_IS_PROVIDED,
|
||||
withComponentInputBinding,
|
||||
withDebugTracing,
|
||||
withDisabledInitialNavigation,
|
||||
withEnabledBlockingInitialNavigation,
|
||||
withPreloading,
|
||||
withViewTransitions,
|
||||
} from './provide_router';
|
||||
import {Router} from './router';
|
||||
import {ExtraOptions, ROUTER_CONFIGURATION} from './router_config';
|
||||
import {RouterConfigLoader, ROUTES} from './router_config_loader';
|
||||
|
|
@ -25,7 +54,6 @@ import {ROUTER_SCROLLER, RouterScroller} from './router_scroller';
|
|||
import {ActivatedRoute} from './router_state';
|
||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||
|
||||
|
||||
/**
|
||||
* The directives defined in the `RouterModule`.
|
||||
*/
|
||||
|
|
@ -35,8 +63,10 @@ const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive, EmptyOutl
|
|||
* @docsNotRequired
|
||||
*/
|
||||
export const ROUTER_FORROOT_GUARD = new InjectionToken<void>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'router duplicate forRoot guard' :
|
||||
'ROUTER_FORROOT_GUARD');
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? 'router duplicate forRoot guard'
|
||||
: 'ROUTER_FORROOT_GUARD',
|
||||
);
|
||||
|
||||
// TODO(atscott): All of these except `ActivatedRoute` are `providedIn: 'root'`. They are only kept
|
||||
// here to avoid a breaking change whereby the provider order matters based on where the
|
||||
|
|
@ -51,8 +81,9 @@ export const ROUTER_PROVIDERS: Provider[] = [
|
|||
RouterConfigLoader,
|
||||
// Only used to warn when `provideRoutes` is used without `RouterModule` or `provideRouter`. Can
|
||||
// be removed when `provideRoutes` is removed.
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? {provide: ROUTER_IS_PROVIDED, useValue: true} :
|
||||
[],
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? {provide: ROUTER_IS_PROVIDED, useValue: true}
|
||||
: [],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -106,14 +137,16 @@ export class RouterModule {
|
|||
ngModule: RouterModule,
|
||||
providers: [
|
||||
ROUTER_PROVIDERS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ?
|
||||
(config?.enableTracing ? withDebugTracing().ɵproviders : []) :
|
||||
[],
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? config?.enableTracing
|
||||
? withDebugTracing().ɵproviders
|
||||
: []
|
||||
: [],
|
||||
{provide: ROUTES, multi: true, useValue: routes},
|
||||
{
|
||||
provide: ROUTER_FORROOT_GUARD,
|
||||
useFactory: provideForRootGuard,
|
||||
deps: [[Router, new Optional(), new SkipSelf()]]
|
||||
deps: [[Router, new Optional(), new SkipSelf()]],
|
||||
},
|
||||
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}},
|
||||
config?.useHash ? provideHashLocationStrategy() : providePathLocationStrategy(),
|
||||
|
|
@ -187,9 +220,10 @@ function providePathLocationStrategy(): Provider {
|
|||
export function provideForRootGuard(router: Router): any {
|
||||
if ((typeof ngDevMode === 'undefined' || ngDevMode) && router) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.FOR_ROOT_CALLED_TWICE,
|
||||
`The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector.` +
|
||||
` Lazy loaded modules should use RouterModule.forChild() instead.`);
|
||||
RuntimeErrorCode.FOR_ROOT_CALLED_TWICE,
|
||||
`The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector.` +
|
||||
` Lazy loaded modules should use RouterModule.forChild() instead.`,
|
||||
);
|
||||
}
|
||||
return 'guarded';
|
||||
}
|
||||
|
|
@ -199,9 +233,9 @@ export function provideForRootGuard(router: Router): any {
|
|||
function provideInitialNavigation(config: Pick<ExtraOptions, 'initialNavigation'>): Provider[] {
|
||||
return [
|
||||
config.initialNavigation === 'disabled' ? withDisabledInitialNavigation().ɵproviders : [],
|
||||
config.initialNavigation === 'enabledBlocking' ?
|
||||
withEnabledBlockingInitialNavigation().ɵproviders :
|
||||
[],
|
||||
config.initialNavigation === 'enabledBlocking'
|
||||
? withEnabledBlockingInitialNavigation().ɵproviders
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +247,8 @@ function provideInitialNavigation(config: Pick<ExtraOptions, 'initialNavigation'
|
|||
* @publicApi
|
||||
*/
|
||||
export const ROUTER_INITIALIZER = new InjectionToken<(compRef: ComponentRef<any>) => void>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'Router Initializer' : '');
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? 'Router Initializer' : '',
|
||||
);
|
||||
|
||||
function provideRouterInitializer(): Provider[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -11,18 +11,17 @@ import {ComponentRef, EnvironmentInjector, Injectable} from '@angular/core';
|
|||
import {RouterOutletContract} from './directives/router_outlet';
|
||||
import {ActivatedRoute} from './router_state';
|
||||
|
||||
|
||||
/**
|
||||
* Store contextual information about a `RouterOutlet`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export class OutletContext {
|
||||
outlet: RouterOutletContract|null = null;
|
||||
route: ActivatedRoute|null = null;
|
||||
injector: EnvironmentInjector|null = null;
|
||||
outlet: RouterOutletContract | null = null;
|
||||
route: ActivatedRoute | null = null;
|
||||
injector: EnvironmentInjector | null = null;
|
||||
children = new ChildrenOutletContexts();
|
||||
attachRef: ComponentRef<any>|null = null;
|
||||
attachRef: ComponentRef<any> | null = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -80,7 +79,7 @@ export class ChildrenOutletContexts {
|
|||
return context;
|
||||
}
|
||||
|
||||
getContext(childName: string): OutletContext|null {
|
||||
getContext(childName: string): OutletContext | null {
|
||||
return this.contexts.get(childName) || null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Compiler, createEnvironmentInjector, EnvironmentInjector, Injectable, OnDestroy} from '@angular/core';
|
||||
import {
|
||||
Compiler,
|
||||
createEnvironmentInjector,
|
||||
EnvironmentInjector,
|
||||
Injectable,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import {from, Observable, of, Subscription} from 'rxjs';
|
||||
import {catchError, concatMap, filter, mergeAll, mergeMap} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -15,7 +21,6 @@ import {LoadedRouterConfig, Route, Routes} from './models';
|
|||
import {Router} from './router';
|
||||
import {RouterConfigLoader} from './router_config_loader';
|
||||
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
|
|
@ -78,14 +83,20 @@ export class RouterPreloader implements OnDestroy {
|
|||
private subscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
private router: Router, compiler: Compiler, private injector: EnvironmentInjector,
|
||||
private preloadingStrategy: PreloadingStrategy, private loader: RouterConfigLoader) {}
|
||||
private router: Router,
|
||||
compiler: Compiler,
|
||||
private injector: EnvironmentInjector,
|
||||
private preloadingStrategy: PreloadingStrategy,
|
||||
private loader: RouterConfigLoader,
|
||||
) {}
|
||||
|
||||
setUpPreloading(): void {
|
||||
this.subscription =
|
||||
this.router.events
|
||||
.pipe(filter((e: Event) => e instanceof NavigationEnd), concatMap(() => this.preload()))
|
||||
.subscribe(() => {});
|
||||
this.subscription = this.router.events
|
||||
.pipe(
|
||||
filter((e: Event) => e instanceof NavigationEnd),
|
||||
concatMap(() => this.preload()),
|
||||
)
|
||||
.subscribe(() => {});
|
||||
}
|
||||
|
||||
preload(): Observable<any> {
|
||||
|
|
@ -103,8 +114,11 @@ export class RouterPreloader implements OnDestroy {
|
|||
const res: Observable<any>[] = [];
|
||||
for (const route of routes) {
|
||||
if (route.providers && !route._injector) {
|
||||
route._injector =
|
||||
createEnvironmentInjector(route.providers, injector, `Route: ${route.path}`);
|
||||
route._injector = createEnvironmentInjector(
|
||||
route.providers,
|
||||
injector,
|
||||
`Route: ${route.path}`,
|
||||
);
|
||||
}
|
||||
|
||||
const injectorForCurrentRoute = route._injector ?? injector;
|
||||
|
|
@ -118,8 +132,10 @@ export class RouterPreloader implements OnDestroy {
|
|||
// when present. Lastly, it remains to be decided whether `canLoad` should behave this way
|
||||
// at all. Code splitting and lazy loading is separate from client-side authorization checks
|
||||
// and should not be used as a security measure to prevent loading of code.
|
||||
if ((route.loadChildren && !route._loadedRoutes && route.canLoad === undefined) ||
|
||||
(route.loadComponent && !route._loadedComponent)) {
|
||||
if (
|
||||
(route.loadChildren && !route._loadedRoutes && route.canLoad === undefined) ||
|
||||
(route.loadComponent && !route._loadedComponent)
|
||||
) {
|
||||
res.push(this.preloadConfig(injectorForCurrentRoute, route));
|
||||
}
|
||||
if (route.children || route._loadedRoutes) {
|
||||
|
|
@ -131,24 +147,25 @@ export class RouterPreloader implements OnDestroy {
|
|||
|
||||
private preloadConfig(injector: EnvironmentInjector, route: Route): Observable<void> {
|
||||
return this.preloadingStrategy.preload(route, () => {
|
||||
let loadedChildren$: Observable<LoadedRouterConfig|null>;
|
||||
let loadedChildren$: Observable<LoadedRouterConfig | null>;
|
||||
if (route.loadChildren && route.canLoad === undefined) {
|
||||
loadedChildren$ = this.loader.loadChildren(injector, route);
|
||||
} else {
|
||||
loadedChildren$ = of(null);
|
||||
}
|
||||
|
||||
const recursiveLoadChildren$ =
|
||||
loadedChildren$.pipe(mergeMap((config: LoadedRouterConfig|null) => {
|
||||
if (config === null) {
|
||||
return of(void 0);
|
||||
}
|
||||
route._loadedRoutes = config.routes;
|
||||
route._loadedInjector = config.injector;
|
||||
// If the loaded config was a module, use that as the module/module injector going
|
||||
// forward. Otherwise, continue using the current module/module injector.
|
||||
return this.processRoutes(config.injector ?? injector, config.routes);
|
||||
}));
|
||||
const recursiveLoadChildren$ = loadedChildren$.pipe(
|
||||
mergeMap((config: LoadedRouterConfig | null) => {
|
||||
if (config === null) {
|
||||
return of(void 0);
|
||||
}
|
||||
route._loadedRoutes = config.routes;
|
||||
route._loadedInjector = config.injector;
|
||||
// If the loaded config was a module, use that as the module/module injector going
|
||||
// forward. Otherwise, continue using the current module/module injector.
|
||||
return this.processRoutes(config.injector ?? injector, config.routes);
|
||||
}),
|
||||
);
|
||||
if (route.loadComponent && !route._loadedComponent) {
|
||||
const loadComponent$ = this.loader.loadComponent(route);
|
||||
return from([recursiveLoadChildren$, loadComponent$]).pipe(mergeAll());
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ import {ViewportScroller} from '@angular/common';
|
|||
import {Injectable, InjectionToken, NgZone, OnDestroy} from '@angular/core';
|
||||
import {Unsubscribable} from 'rxjs';
|
||||
|
||||
import {NavigationEnd, NavigationSkipped, NavigationSkippedCode, NavigationStart, Scroll} from './events';
|
||||
import {
|
||||
NavigationEnd,
|
||||
NavigationSkipped,
|
||||
NavigationSkippedCode,
|
||||
NavigationStart,
|
||||
Scroll,
|
||||
} from './events';
|
||||
import {NavigationTransitions} from './navigation_transition';
|
||||
import {UrlSerializer} from './url_tree';
|
||||
|
||||
|
|
@ -22,18 +28,21 @@ export class RouterScroller implements OnDestroy {
|
|||
private scrollEventsSubscription?: Unsubscribable;
|
||||
|
||||
private lastId = 0;
|
||||
private lastSource: 'imperative'|'popstate'|'hashchange'|undefined = 'imperative';
|
||||
private lastSource: 'imperative' | 'popstate' | 'hashchange' | undefined = 'imperative';
|
||||
private restoredId = 0;
|
||||
private store: {[key: string]: [number, number]} = {};
|
||||
|
||||
/** @nodoc */
|
||||
constructor(
|
||||
readonly urlSerializer: UrlSerializer, private transitions: NavigationTransitions,
|
||||
public readonly viewportScroller: ViewportScroller, private readonly zone: NgZone,
|
||||
private options: {
|
||||
scrollPositionRestoration?: 'disabled'|'enabled'|'top',
|
||||
anchorScrolling?: 'disabled'|'enabled'
|
||||
} = {}) {
|
||||
readonly urlSerializer: UrlSerializer,
|
||||
private transitions: NavigationTransitions,
|
||||
public readonly viewportScroller: ViewportScroller,
|
||||
private readonly zone: NgZone,
|
||||
private options: {
|
||||
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
|
||||
anchorScrolling?: 'disabled' | 'enabled';
|
||||
} = {},
|
||||
) {
|
||||
// Default both options to 'disabled'
|
||||
options.scrollPositionRestoration ||= 'disabled';
|
||||
options.anchorScrolling ||= 'disabled';
|
||||
|
|
@ -51,7 +60,7 @@ export class RouterScroller implements OnDestroy {
|
|||
}
|
||||
|
||||
private createScrollEvents() {
|
||||
return this.transitions.events.subscribe(e => {
|
||||
return this.transitions.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
// store the scroll position of the current stable navigations.
|
||||
this.store[this.lastId] = this.viewportScroller.getScrollPosition();
|
||||
|
|
@ -61,8 +70,9 @@ export class RouterScroller implements OnDestroy {
|
|||
this.lastId = e.id;
|
||||
this.scheduleScrollEvent(e, this.urlSerializer.parse(e.urlAfterRedirects).fragment);
|
||||
} else if (
|
||||
e instanceof NavigationSkipped &&
|
||||
e.code === NavigationSkippedCode.IgnoredSameUrlNavigation) {
|
||||
e instanceof NavigationSkipped &&
|
||||
e.code === NavigationSkippedCode.IgnoredSameUrlNavigation
|
||||
) {
|
||||
this.lastSource = undefined;
|
||||
this.restoredId = 0;
|
||||
this.scheduleScrollEvent(e, this.urlSerializer.parse(e.url).fragment);
|
||||
|
|
@ -71,7 +81,7 @@ export class RouterScroller implements OnDestroy {
|
|||
}
|
||||
|
||||
private consumeScrollEvents() {
|
||||
return this.transitions.events.subscribe(e => {
|
||||
return this.transitions.events.subscribe((e) => {
|
||||
if (!(e instanceof Scroll)) return;
|
||||
// a popstate event. The pop state event will always ignore anchor scrolling.
|
||||
if (e.position) {
|
||||
|
|
@ -91,17 +101,23 @@ export class RouterScroller implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
private scheduleScrollEvent(routerEvent: NavigationEnd|NavigationSkipped, anchor: string|null):
|
||||
void {
|
||||
private scheduleScrollEvent(
|
||||
routerEvent: NavigationEnd | NavigationSkipped,
|
||||
anchor: string | null,
|
||||
): void {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
// The scroll event needs to be delayed until after change detection. Otherwise, we may
|
||||
// attempt to restore the scroll position before the router outlet has fully rendered the
|
||||
// component by executing its update block of the template function.
|
||||
setTimeout(() => {
|
||||
this.zone.run(() => {
|
||||
this.transitions.events.next(new Scroll(
|
||||
routerEvent, this.lastSource === 'popstate' ? this.store[this.restoredId] : null,
|
||||
anchor));
|
||||
this.transitions.events.next(
|
||||
new Scroll(
|
||||
routerEvent,
|
||||
this.lastSource === 'popstate' ? this.store[this.restoredId] : null,
|
||||
anchor,
|
||||
),
|
||||
);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,9 +50,10 @@ import {Tree, TreeNode} from './utils/tree';
|
|||
export class RouterState extends Tree<ActivatedRoute> {
|
||||
/** @internal */
|
||||
constructor(
|
||||
root: TreeNode<ActivatedRoute>,
|
||||
/** The current snapshot of the router state */
|
||||
public snapshot: RouterStateSnapshot) {
|
||||
root: TreeNode<ActivatedRoute>,
|
||||
/** The current snapshot of the router state */
|
||||
public snapshot: RouterStateSnapshot,
|
||||
) {
|
||||
super(root);
|
||||
setRouterState(<RouterState>this, root);
|
||||
}
|
||||
|
|
@ -62,28 +63,43 @@ export class RouterState extends Tree<ActivatedRoute> {
|
|||
}
|
||||
}
|
||||
|
||||
export function createEmptyState(rootComponent: Type<any>|null): RouterState {
|
||||
export function createEmptyState(rootComponent: Type<any> | null): RouterState {
|
||||
const snapshot = createEmptyStateSnapshot(rootComponent);
|
||||
const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]);
|
||||
const emptyParams = new BehaviorSubject({});
|
||||
const emptyData = new BehaviorSubject({});
|
||||
const emptyQueryParams = new BehaviorSubject({});
|
||||
const fragment = new BehaviorSubject<string|null>('');
|
||||
const fragment = new BehaviorSubject<string | null>('');
|
||||
const activated = new ActivatedRoute(
|
||||
emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent,
|
||||
snapshot.root);
|
||||
emptyUrl,
|
||||
emptyParams,
|
||||
emptyQueryParams,
|
||||
fragment,
|
||||
emptyData,
|
||||
PRIMARY_OUTLET,
|
||||
rootComponent,
|
||||
snapshot.root,
|
||||
);
|
||||
activated.snapshot = snapshot.root;
|
||||
return new RouterState(new TreeNode<ActivatedRoute>(activated, []), snapshot);
|
||||
}
|
||||
|
||||
export function createEmptyStateSnapshot(rootComponent: Type<any>|null): RouterStateSnapshot {
|
||||
export function createEmptyStateSnapshot(rootComponent: Type<any> | null): RouterStateSnapshot {
|
||||
const emptyParams = {};
|
||||
const emptyData = {};
|
||||
const emptyQueryParams = {};
|
||||
const fragment = '';
|
||||
const activated = new ActivatedRouteSnapshot(
|
||||
[], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null,
|
||||
{});
|
||||
[],
|
||||
emptyParams,
|
||||
emptyQueryParams,
|
||||
fragment,
|
||||
emptyData,
|
||||
PRIMARY_OUTLET,
|
||||
rootComponent,
|
||||
null,
|
||||
{},
|
||||
);
|
||||
return new RouterStateSnapshot('', new TreeNode<ActivatedRouteSnapshot>(activated, []));
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +135,7 @@ export class ActivatedRoute {
|
|||
_queryParamMap?: Observable<ParamMap>;
|
||||
|
||||
/** An Observable of the resolved route title */
|
||||
readonly title: Observable<string|undefined>;
|
||||
readonly title: Observable<string | undefined>;
|
||||
|
||||
/** An observable of the URL segments matched by this route. */
|
||||
public url: Observable<UrlSegment[]>;
|
||||
|
|
@ -128,26 +144,28 @@ export class ActivatedRoute {
|
|||
/** An observable of the query parameters shared by all the routes. */
|
||||
public queryParams: Observable<Params>;
|
||||
/** An observable of the URL fragment shared by all the routes. */
|
||||
public fragment: Observable<string|null>;
|
||||
public fragment: Observable<string | null>;
|
||||
/** An observable of the static and resolved data of this route. */
|
||||
public data: Observable<Data>;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** @internal */
|
||||
public urlSubject: BehaviorSubject<UrlSegment[]>,
|
||||
/** @internal */
|
||||
public paramsSubject: BehaviorSubject<Params>,
|
||||
/** @internal */
|
||||
public queryParamsSubject: BehaviorSubject<Params>,
|
||||
/** @internal */
|
||||
public fragmentSubject: BehaviorSubject<string|null>,
|
||||
/** @internal */
|
||||
public dataSubject: BehaviorSubject<Data>,
|
||||
/** The outlet name of the route, a constant. */
|
||||
public outlet: string,
|
||||
/** The component of the route, a constant. */
|
||||
public component: Type<any>|null, futureSnapshot: ActivatedRouteSnapshot) {
|
||||
/** @internal */
|
||||
public urlSubject: BehaviorSubject<UrlSegment[]>,
|
||||
/** @internal */
|
||||
public paramsSubject: BehaviorSubject<Params>,
|
||||
/** @internal */
|
||||
public queryParamsSubject: BehaviorSubject<Params>,
|
||||
/** @internal */
|
||||
public fragmentSubject: BehaviorSubject<string | null>,
|
||||
/** @internal */
|
||||
public dataSubject: BehaviorSubject<Data>,
|
||||
/** The outlet name of the route, a constant. */
|
||||
public outlet: string,
|
||||
/** The component of the route, a constant. */
|
||||
public component: Type<any> | null,
|
||||
futureSnapshot: ActivatedRouteSnapshot,
|
||||
) {
|
||||
this._futureSnapshot = futureSnapshot;
|
||||
this.title = this.dataSubject?.pipe(map((d: Data) => d[RouteTitleKey])) ?? of(undefined);
|
||||
// TODO(atscott): Verify that these can be changed to `.asObservable()` with TGP.
|
||||
|
|
@ -159,7 +177,7 @@ export class ActivatedRoute {
|
|||
}
|
||||
|
||||
/** The configuration used to match this route. */
|
||||
get routeConfig(): Route|null {
|
||||
get routeConfig(): Route | null {
|
||||
return this._futureSnapshot.routeConfig;
|
||||
}
|
||||
|
||||
|
|
@ -169,12 +187,12 @@ export class ActivatedRoute {
|
|||
}
|
||||
|
||||
/** The parent of this route in the router state tree. */
|
||||
get parent(): ActivatedRoute|null {
|
||||
get parent(): ActivatedRoute | null {
|
||||
return this._routerState.parent(this);
|
||||
}
|
||||
|
||||
/** The first child of this route in the router state tree. */
|
||||
get firstChild(): ActivatedRoute|null {
|
||||
get firstChild(): ActivatedRoute | null {
|
||||
return this._routerState.firstChild(this);
|
||||
}
|
||||
|
||||
|
|
@ -203,8 +221,9 @@ export class ActivatedRoute {
|
|||
* The map supports retrieving single and multiple values from the query parameter.
|
||||
*/
|
||||
get queryParamMap(): Observable<ParamMap> {
|
||||
this._queryParamMap ??=
|
||||
this.queryParams.pipe(map((p: Params): ParamMap => convertToParamMap(p)));
|
||||
this._queryParamMap ??= this.queryParams.pipe(
|
||||
map((p: Params): ParamMap => convertToParamMap(p)),
|
||||
);
|
||||
return this._queryParamMap;
|
||||
}
|
||||
|
||||
|
|
@ -213,13 +232,13 @@ export class ActivatedRoute {
|
|||
}
|
||||
}
|
||||
|
||||
export type ParamsInheritanceStrategy = 'emptyOnly'|'always';
|
||||
export type ParamsInheritanceStrategy = 'emptyOnly' | 'always';
|
||||
|
||||
/** @internal */
|
||||
export type Inherited = {
|
||||
params: Params,
|
||||
data: Data,
|
||||
resolve: Data,
|
||||
params: Params;
|
||||
data: Data;
|
||||
resolve: Data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -229,16 +248,20 @@ export type Inherited = {
|
|||
* route is component-less.
|
||||
*/
|
||||
export function getInherited(
|
||||
route: ActivatedRouteSnapshot, parent: ActivatedRouteSnapshot|null,
|
||||
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly'): Inherited {
|
||||
route: ActivatedRouteSnapshot,
|
||||
parent: ActivatedRouteSnapshot | null,
|
||||
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly',
|
||||
): Inherited {
|
||||
let inherited: Inherited;
|
||||
const {routeConfig} = route;
|
||||
if (parent !== null &&
|
||||
(paramsInheritanceStrategy === 'always' ||
|
||||
// inherit parent data if route is empty path
|
||||
routeConfig?.path === '' ||
|
||||
// inherit parent data if parent was componentless
|
||||
(!parent.component && !parent.routeConfig?.loadComponent))) {
|
||||
if (
|
||||
parent !== null &&
|
||||
(paramsInheritanceStrategy === 'always' ||
|
||||
// inherit parent data if route is empty path
|
||||
routeConfig?.path === '' ||
|
||||
// inherit parent data if parent was componentless
|
||||
(!parent.component && !parent.routeConfig?.loadComponent))
|
||||
) {
|
||||
inherited = {
|
||||
params: {...parent.params, ...route.params},
|
||||
data: {...parent.data, ...route.data},
|
||||
|
|
@ -256,13 +279,13 @@ export function getInherited(
|
|||
...routeConfig?.data,
|
||||
// resolved data from current route overrides everything
|
||||
...route._resolvedData,
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
inherited = {
|
||||
params: {...route.params},
|
||||
data: {...route.data},
|
||||
resolve: {...route.data, ...(route._resolvedData ?? {})}
|
||||
resolve: {...route.data, ...(route._resolvedData ?? {})},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +320,7 @@ export function getInherited(
|
|||
*/
|
||||
export class ActivatedRouteSnapshot {
|
||||
/** The configuration used to match this route **/
|
||||
public readonly routeConfig: Route|null;
|
||||
public readonly routeConfig: Route | null;
|
||||
/** @internal */
|
||||
_resolve: ResolveData;
|
||||
/** @internal */
|
||||
|
|
@ -310,7 +333,7 @@ export class ActivatedRouteSnapshot {
|
|||
_queryParamMap?: ParamMap;
|
||||
|
||||
/** The resolved route title */
|
||||
get title(): string|undefined {
|
||||
get title(): string | undefined {
|
||||
// Note: This _must_ be a getter because the data is mutated in the resolvers. Title will not be
|
||||
// available at the time of class instantiation.
|
||||
return this.data?.[RouteTitleKey];
|
||||
|
|
@ -318,38 +341,41 @@ export class ActivatedRouteSnapshot {
|
|||
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The URL segments matched by this route */
|
||||
public url: UrlSegment[],
|
||||
/**
|
||||
* The matrix parameters scoped to this route.
|
||||
*
|
||||
* You can compute all params (or data) in the router state or to get params outside
|
||||
* of an activated component by traversing the `RouterState` tree as in the following
|
||||
* example:
|
||||
* ```
|
||||
* collectRouteParams(router: Router) {
|
||||
* let params = {};
|
||||
* let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root];
|
||||
* while (stack.length > 0) {
|
||||
* const route = stack.pop()!;
|
||||
* params = {...params, ...route.params};
|
||||
* stack.push(...route.children);
|
||||
* }
|
||||
* return params;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public params: Params,
|
||||
/** The query parameters shared by all the routes */
|
||||
public queryParams: Params,
|
||||
/** The URL fragment shared by all the routes */
|
||||
public fragment: string|null,
|
||||
/** The static and resolved data of this route */
|
||||
public data: Data,
|
||||
/** The outlet name of the route */
|
||||
public outlet: string,
|
||||
/** The component of the route */
|
||||
public component: Type<any>|null, routeConfig: Route|null, resolve: ResolveData) {
|
||||
/** The URL segments matched by this route */
|
||||
public url: UrlSegment[],
|
||||
/**
|
||||
* The matrix parameters scoped to this route.
|
||||
*
|
||||
* You can compute all params (or data) in the router state or to get params outside
|
||||
* of an activated component by traversing the `RouterState` tree as in the following
|
||||
* example:
|
||||
* ```
|
||||
* collectRouteParams(router: Router) {
|
||||
* let params = {};
|
||||
* let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root];
|
||||
* while (stack.length > 0) {
|
||||
* const route = stack.pop()!;
|
||||
* params = {...params, ...route.params};
|
||||
* stack.push(...route.children);
|
||||
* }
|
||||
* return params;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public params: Params,
|
||||
/** The query parameters shared by all the routes */
|
||||
public queryParams: Params,
|
||||
/** The URL fragment shared by all the routes */
|
||||
public fragment: string | null,
|
||||
/** The static and resolved data of this route */
|
||||
public data: Data,
|
||||
/** The outlet name of the route */
|
||||
public outlet: string,
|
||||
/** The component of the route */
|
||||
public component: Type<any> | null,
|
||||
routeConfig: Route | null,
|
||||
resolve: ResolveData,
|
||||
) {
|
||||
this.routeConfig = routeConfig;
|
||||
this._resolve = resolve;
|
||||
}
|
||||
|
|
@ -360,12 +386,12 @@ export class ActivatedRouteSnapshot {
|
|||
}
|
||||
|
||||
/** The parent of this route in the router state tree */
|
||||
get parent(): ActivatedRouteSnapshot|null {
|
||||
get parent(): ActivatedRouteSnapshot | null {
|
||||
return this._routerState.parent(this);
|
||||
}
|
||||
|
||||
/** The first child of this route in the router state tree */
|
||||
get firstChild(): ActivatedRouteSnapshot|null {
|
||||
get firstChild(): ActivatedRouteSnapshot | null {
|
||||
return this._routerState.firstChild(this);
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +416,7 @@ export class ActivatedRouteSnapshot {
|
|||
}
|
||||
|
||||
toString(): string {
|
||||
const url = this.url.map(segment => segment.toString()).join('/');
|
||||
const url = this.url.map((segment) => segment.toString()).join('/');
|
||||
const matched = this.routeConfig ? this.routeConfig.path : '';
|
||||
return `Route(url:'${url}', path:'${matched}')`;
|
||||
}
|
||||
|
|
@ -426,8 +452,10 @@ export class ActivatedRouteSnapshot {
|
|||
export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The url from which this snapshot was created */
|
||||
public url: string, root: TreeNode<ActivatedRouteSnapshot>) {
|
||||
/** The url from which this snapshot was created */
|
||||
public url: string,
|
||||
root: TreeNode<ActivatedRouteSnapshot>,
|
||||
) {
|
||||
super(root);
|
||||
setRouterState(<RouterStateSnapshot>this, root);
|
||||
}
|
||||
|
|
@ -439,7 +467,7 @@ export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
|
|||
|
||||
function setRouterState<U, T extends {_routerState: U}>(state: U, node: TreeNode<T>): void {
|
||||
node.value._routerState = state;
|
||||
node.children.forEach(c => setRouterState(state, c));
|
||||
node.children.forEach((c) => setRouterState(state, c));
|
||||
}
|
||||
|
||||
function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
|
||||
|
|
@ -480,14 +508,18 @@ export function advanceActivatedRoute(route: ActivatedRoute): void {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function equalParamsAndUrlSegments(
|
||||
a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
||||
a: ActivatedRouteSnapshot,
|
||||
b: ActivatedRouteSnapshot,
|
||||
): boolean {
|
||||
const equalUrlParams = shallowEqual(a.params, b.params) && equalSegments(a.url, b.url);
|
||||
const parentsMismatch = !a.parent !== !b.parent;
|
||||
|
||||
return equalUrlParams && !parentsMismatch &&
|
||||
(!a.parent || equalParamsAndUrlSegments(a.parent, b.parent!));
|
||||
return (
|
||||
equalUrlParams &&
|
||||
!parentsMismatch &&
|
||||
(!a.parent || equalParamsAndUrlSegments(a.parent, b.parent!))
|
||||
);
|
||||
}
|
||||
|
||||
export function hasStaticTitle(config: Route) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
import {Route, UrlMatchResult} from './models';
|
||||
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
||||
|
||||
|
||||
/**
|
||||
* The primary routing outlet.
|
||||
*
|
||||
|
|
@ -59,7 +58,7 @@ export interface ParamMap {
|
|||
* or the first value if the parameter has multiple values,
|
||||
* or `null` when there is no such parameter.
|
||||
*/
|
||||
get(name: string): string|null;
|
||||
get(name: string): string | null;
|
||||
/**
|
||||
* Retrieves multiple values for a parameter.
|
||||
* @param name The parameter name.
|
||||
|
|
@ -84,7 +83,7 @@ class ParamsAsMap implements ParamMap {
|
|||
return Object.prototype.hasOwnProperty.call(this.params, name);
|
||||
}
|
||||
|
||||
get(name: string): string|null {
|
||||
get(name: string): string | null {
|
||||
if (this.has(name)) {
|
||||
const v = this.params[name];
|
||||
return Array.isArray(v) ? v[0] : v;
|
||||
|
|
@ -134,7 +133,10 @@ export function convertToParamMap(params: Params): ParamMap {
|
|||
* @publicApi
|
||||
*/
|
||||
export function defaultUrlMatcher(
|
||||
segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult|null {
|
||||
segments: UrlSegment[],
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
route: Route,
|
||||
): UrlMatchResult | null {
|
||||
const parts = route.path!.split('/');
|
||||
|
||||
if (parts.length > segments.length) {
|
||||
|
|
@ -142,8 +144,10 @@ export function defaultUrlMatcher(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (route.pathMatch === 'full' &&
|
||||
(segmentGroup.hasChildren() || parts.length < segments.length)) {
|
||||
if (
|
||||
route.pathMatch === 'full' &&
|
||||
(segmentGroup.hasChildren() || parts.length < segments.length)
|
||||
) {
|
||||
// The config is longer than the actual URL but we are looking for a full match, return null
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,18 @@ import {Location} from '@angular/common';
|
|||
import {inject, Injectable} from '@angular/core';
|
||||
import {SubscriptionLike} from 'rxjs';
|
||||
|
||||
import {BeforeActivateRoutes, Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, PrivateRouterEvents, RoutesRecognized,} from '../events';
|
||||
import {
|
||||
BeforeActivateRoutes,
|
||||
Event,
|
||||
NavigationCancel,
|
||||
NavigationCancellationCode,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationSkipped,
|
||||
NavigationStart,
|
||||
PrivateRouterEvents,
|
||||
RoutesRecognized,
|
||||
} from '../events';
|
||||
import {Navigation, RestoredState} from '../navigation_transition';
|
||||
import {ROUTER_CONFIGURATION} from '../router_config';
|
||||
import {createEmptyState, RouterState} from '../router_state';
|
||||
|
|
@ -58,7 +69,7 @@ export abstract class StateManager {
|
|||
abstract getRawUrlTree(): UrlTree;
|
||||
|
||||
/** Returns the current state stored by the browser for the current history entry. */
|
||||
abstract restoredState(): RestoredState|null|undefined;
|
||||
abstract restoredState(): RestoredState | null | undefined;
|
||||
|
||||
/** Returns the current RouterState. */
|
||||
abstract getRouterState(): RouterState;
|
||||
|
|
@ -69,13 +80,14 @@ export abstract class StateManager {
|
|||
* also includes programmatic APIs called by non-Router JavaScript.
|
||||
*/
|
||||
abstract registerNonRouterCurrentEntryChangeListener(
|
||||
listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike;
|
||||
listener: (url: string, state: RestoredState | null | undefined) => void,
|
||||
): SubscriptionLike;
|
||||
|
||||
/**
|
||||
* Handles a navigation event sent from the Router. These are typically events that indicate a
|
||||
* navigation has started, progressed, been cancelled, or finished.
|
||||
*/
|
||||
abstract handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation): void;
|
||||
abstract handleRouterEvent(e: Event | PrivateRouterEvents, currentTransition: Navigation): void;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
|
|
@ -84,7 +96,7 @@ export class HistoryStateManager extends StateManager {
|
|||
private readonly urlSerializer = inject(UrlSerializer);
|
||||
private readonly options = inject(ROUTER_CONFIGURATION, {optional: true}) || {};
|
||||
private readonly canceledNavigationResolution =
|
||||
this.options.canceledNavigationResolution || 'replace';
|
||||
this.options.canceledNavigationResolution || 'replace';
|
||||
|
||||
private urlHandlingStrategy = inject(UrlHandlingStrategy);
|
||||
private urlUpdateStrategy = this.options.urlUpdateStrategy || 'deferred';
|
||||
|
|
@ -112,7 +124,7 @@ export class HistoryStateManager extends StateManager {
|
|||
private currentPageId: number = 0;
|
||||
private lastSuccessfulId: number = -1;
|
||||
|
||||
override restoredState(): RestoredState|null|undefined {
|
||||
override restoredState(): RestoredState | null | undefined {
|
||||
return this.location.getState() as RestoredState | null | undefined;
|
||||
}
|
||||
|
||||
|
|
@ -145,15 +157,16 @@ export class HistoryStateManager extends StateManager {
|
|||
}
|
||||
|
||||
override registerNonRouterCurrentEntryChangeListener(
|
||||
listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike {
|
||||
return this.location.subscribe(event => {
|
||||
listener: (url: string, state: RestoredState | null | undefined) => void,
|
||||
): SubscriptionLike {
|
||||
return this.location.subscribe((event) => {
|
||||
if (event['type'] === 'popstate') {
|
||||
listener(event['url']!, event.state as RestoredState | null | undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation) {
|
||||
override handleRouterEvent(e: Event | PrivateRouterEvents, currentTransition: Navigation) {
|
||||
if (e instanceof NavigationStart) {
|
||||
this.stateMemento = this.createStateMemento();
|
||||
} else if (e instanceof NavigationSkipped) {
|
||||
|
|
@ -162,14 +175,18 @@ export class HistoryStateManager extends StateManager {
|
|||
if (this.urlUpdateStrategy === 'eager') {
|
||||
if (!currentTransition.extras.skipLocationChange) {
|
||||
const rawUrl = this.urlHandlingStrategy.merge(
|
||||
currentTransition.finalUrl!, currentTransition.initialUrl);
|
||||
currentTransition.finalUrl!,
|
||||
currentTransition.initialUrl,
|
||||
);
|
||||
this.setBrowserUrl(rawUrl, currentTransition);
|
||||
}
|
||||
}
|
||||
} else if (e instanceof BeforeActivateRoutes) {
|
||||
this.currentUrlTree = currentTransition.finalUrl!;
|
||||
this.rawUrlTree =
|
||||
this.urlHandlingStrategy.merge(currentTransition.finalUrl!, currentTransition.initialUrl);
|
||||
this.rawUrlTree = this.urlHandlingStrategy.merge(
|
||||
currentTransition.finalUrl!,
|
||||
currentTransition.initialUrl,
|
||||
);
|
||||
this.routerState = currentTransition.targetRouterState!;
|
||||
if (this.urlUpdateStrategy === 'deferred') {
|
||||
if (!currentTransition.extras.skipLocationChange) {
|
||||
|
|
@ -177,9 +194,10 @@ export class HistoryStateManager extends StateManager {
|
|||
}
|
||||
}
|
||||
} else if (
|
||||
e instanceof NavigationCancel &&
|
||||
(e.code === NavigationCancellationCode.GuardRejected ||
|
||||
e.code === NavigationCancellationCode.NoDataFromResolver)) {
|
||||
e instanceof NavigationCancel &&
|
||||
(e.code === NavigationCancellationCode.GuardRejected ||
|
||||
e.code === NavigationCancellationCode.NoDataFromResolver)
|
||||
) {
|
||||
this.restoreHistory(currentTransition);
|
||||
} else if (e instanceof NavigationError) {
|
||||
this.restoreHistory(currentTransition, true);
|
||||
|
|
@ -196,13 +214,13 @@ export class HistoryStateManager extends StateManager {
|
|||
const currentBrowserPageId = this.browserPageId;
|
||||
const state = {
|
||||
...transition.extras.state,
|
||||
...this.generateNgRouterState(transition.id, currentBrowserPageId)
|
||||
...this.generateNgRouterState(transition.id, currentBrowserPageId),
|
||||
};
|
||||
this.location.replaceState(path, '', state);
|
||||
} else {
|
||||
const state = {
|
||||
...transition.extras.state,
|
||||
...this.generateNgRouterState(transition.id, this.browserPageId + 1)
|
||||
...this.generateNgRouterState(transition.id, this.browserPageId + 1),
|
||||
};
|
||||
this.location.go(path, '', state);
|
||||
}
|
||||
|
|
@ -248,14 +266,18 @@ export class HistoryStateManager extends StateManager {
|
|||
// the part of the navigation handled by the Angular router rather than the whole URL. In
|
||||
// addition, the URLHandlingStrategy may be configured to specifically preserve parts of the URL
|
||||
// when merging, such as the query params so they are not lost on a refresh.
|
||||
this.rawUrlTree =
|
||||
this.urlHandlingStrategy.merge(this.currentUrlTree, navigation.finalUrl ?? this.rawUrlTree);
|
||||
this.rawUrlTree = this.urlHandlingStrategy.merge(
|
||||
this.currentUrlTree,
|
||||
navigation.finalUrl ?? this.rawUrlTree,
|
||||
);
|
||||
}
|
||||
|
||||
private resetUrlToCurrentUrlTree(): void {
|
||||
this.location.replaceState(
|
||||
this.urlSerializer.serialize(this.rawUrlTree), '',
|
||||
this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId));
|
||||
this.urlSerializer.serialize(this.rawUrlTree),
|
||||
'',
|
||||
this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId),
|
||||
);
|
||||
}
|
||||
|
||||
private generateNgRouterState(navigationId: number, routerPageId: number) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {RuntimeErrorCode} from './errors';
|
|||
import {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET} from './shared';
|
||||
import {equalArraysOrString, shallowEqual} from './utils/collection';
|
||||
|
||||
|
||||
/**
|
||||
* A set of options which specify how to determine if a `UrlTree` is active, given the `UrlTree`
|
||||
* for the current router state.
|
||||
|
|
@ -34,7 +33,7 @@ export interface IsActiveMatchOptions {
|
|||
* extra matrix parameters, but those that exist in the `UrlTree` in question must match.
|
||||
* - `'ignored'`: When comparing `UrlTree`s, matrix params will be ignored.
|
||||
*/
|
||||
matrixParams: 'exact'|'subset'|'ignored';
|
||||
matrixParams: 'exact' | 'subset' | 'ignored';
|
||||
/**
|
||||
* Defines the strategy for comparing the query parameters of two `UrlTree`s.
|
||||
*
|
||||
|
|
@ -43,7 +42,7 @@ export interface IsActiveMatchOptions {
|
|||
* but must match the key and value of any that exist in the `UrlTree` in question.
|
||||
* - `'ignored'`: When comparing `UrlTree`s, query params will be ignored.
|
||||
*/
|
||||
queryParams: 'exact'|'subset'|'ignored';
|
||||
queryParams: 'exact' | 'subset' | 'ignored';
|
||||
/**
|
||||
* Defines the strategy for comparing the `UrlSegment`s of the `UrlTree`s.
|
||||
*
|
||||
|
|
@ -52,20 +51,22 @@ export interface IsActiveMatchOptions {
|
|||
* is a subtree of the active route. That is, the active route may contain extra
|
||||
* segments, but must at least have all the segments of the `UrlTree` in question.
|
||||
*/
|
||||
paths: 'exact'|'subset';
|
||||
paths: 'exact' | 'subset';
|
||||
/**
|
||||
* - `'exact'`: indicates that the `UrlTree` fragments must be equal.
|
||||
* - `'ignored'`: the fragments will not be compared when determining if a
|
||||
* `UrlTree` is active.
|
||||
*/
|
||||
fragment: 'exact'|'ignored';
|
||||
fragment: 'exact' | 'ignored';
|
||||
}
|
||||
|
||||
type ParamMatchOptions = 'exact'|'subset'|'ignored';
|
||||
type ParamMatchOptions = 'exact' | 'subset' | 'ignored';
|
||||
|
||||
type PathCompareFn =
|
||||
(container: UrlSegmentGroup, containee: UrlSegmentGroup, matrixParams: ParamMatchOptions) =>
|
||||
boolean;
|
||||
type PathCompareFn = (
|
||||
container: UrlSegmentGroup,
|
||||
containee: UrlSegmentGroup,
|
||||
matrixParams: ParamMatchOptions,
|
||||
) => boolean;
|
||||
type ParamCompareFn = (container: Params, containee: Params) => boolean;
|
||||
|
||||
const pathCompareMap: Record<IsActiveMatchOptions['paths'], PathCompareFn> = {
|
||||
|
|
@ -79,10 +80,15 @@ const paramCompareMap: Record<ParamMatchOptions, ParamCompareFn> = {
|
|||
};
|
||||
|
||||
export function containsTree(
|
||||
container: UrlTree, containee: UrlTree, options: IsActiveMatchOptions): boolean {
|
||||
return pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) &&
|
||||
paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) &&
|
||||
!(options.fragment === 'exact' && container.fragment !== containee.fragment);
|
||||
container: UrlTree,
|
||||
containee: UrlTree,
|
||||
options: IsActiveMatchOptions,
|
||||
): boolean {
|
||||
return (
|
||||
pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) &&
|
||||
paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) &&
|
||||
!(options.fragment === 'exact' && container.fragment !== containee.fragment)
|
||||
);
|
||||
}
|
||||
|
||||
function equalParams(container: Params, containee: Params): boolean {
|
||||
|
|
@ -91,8 +97,10 @@ function equalParams(container: Params, containee: Params): boolean {
|
|||
}
|
||||
|
||||
function equalSegmentGroups(
|
||||
container: UrlSegmentGroup, containee: UrlSegmentGroup,
|
||||
matrixParams: ParamMatchOptions): boolean {
|
||||
container: UrlSegmentGroup,
|
||||
containee: UrlSegmentGroup,
|
||||
matrixParams: ParamMatchOptions,
|
||||
): boolean {
|
||||
if (!equalPath(container.segments, containee.segments)) return false;
|
||||
if (!matrixParamsMatch(container.segments, containee.segments, matrixParams)) {
|
||||
return false;
|
||||
|
|
@ -107,26 +115,32 @@ function equalSegmentGroups(
|
|||
}
|
||||
|
||||
function containsParams(container: Params, containee: Params): boolean {
|
||||
return Object.keys(containee).length <= Object.keys(container).length &&
|
||||
Object.keys(containee).every(key => equalArraysOrString(container[key], containee[key]));
|
||||
return (
|
||||
Object.keys(containee).length <= Object.keys(container).length &&
|
||||
Object.keys(containee).every((key) => equalArraysOrString(container[key], containee[key]))
|
||||
);
|
||||
}
|
||||
|
||||
function containsSegmentGroup(
|
||||
container: UrlSegmentGroup, containee: UrlSegmentGroup,
|
||||
matrixParams: ParamMatchOptions): boolean {
|
||||
container: UrlSegmentGroup,
|
||||
containee: UrlSegmentGroup,
|
||||
matrixParams: ParamMatchOptions,
|
||||
): boolean {
|
||||
return containsSegmentGroupHelper(container, containee, containee.segments, matrixParams);
|
||||
}
|
||||
|
||||
function containsSegmentGroupHelper(
|
||||
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[],
|
||||
matrixParams: ParamMatchOptions): boolean {
|
||||
container: UrlSegmentGroup,
|
||||
containee: UrlSegmentGroup,
|
||||
containeePaths: UrlSegment[],
|
||||
matrixParams: ParamMatchOptions,
|
||||
): boolean {
|
||||
if (container.segments.length > containeePaths.length) {
|
||||
const current = container.segments.slice(0, containeePaths.length);
|
||||
if (!equalPath(current, containeePaths)) return false;
|
||||
if (containee.hasChildren()) return false;
|
||||
if (!matrixParamsMatch(current, containeePaths, matrixParams)) return false;
|
||||
return true;
|
||||
|
||||
} else if (container.segments.length === containeePaths.length) {
|
||||
if (!equalPath(container.segments, containeePaths)) return false;
|
||||
if (!matrixParamsMatch(container.segments, containeePaths, matrixParams)) return false;
|
||||
|
|
@ -137,7 +151,6 @@ function containsSegmentGroupHelper(
|
|||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else {
|
||||
const current = containeePaths.slice(0, container.segments.length);
|
||||
const next = containeePaths.slice(container.segments.length);
|
||||
|
|
@ -145,12 +158,19 @@ function containsSegmentGroupHelper(
|
|||
if (!matrixParamsMatch(container.segments, current, matrixParams)) return false;
|
||||
if (!container.children[PRIMARY_OUTLET]) return false;
|
||||
return containsSegmentGroupHelper(
|
||||
container.children[PRIMARY_OUTLET], containee, next, matrixParams);
|
||||
container.children[PRIMARY_OUTLET],
|
||||
containee,
|
||||
next,
|
||||
matrixParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function matrixParamsMatch(
|
||||
containerPaths: UrlSegment[], containeePaths: UrlSegment[], options: ParamMatchOptions) {
|
||||
containerPaths: UrlSegment[],
|
||||
containeePaths: UrlSegment[],
|
||||
options: ParamMatchOptions,
|
||||
) {
|
||||
return containeePaths.every((containeeSegment, i) => {
|
||||
return paramCompareMap[options](containerPaths[i].parameters, containeeSegment.parameters);
|
||||
});
|
||||
|
|
@ -191,18 +211,20 @@ export class UrlTree {
|
|||
_queryParamMap?: ParamMap;
|
||||
|
||||
constructor(
|
||||
/** The root segment group of the URL tree */
|
||||
public root: UrlSegmentGroup = new UrlSegmentGroup([], {}),
|
||||
/** The query params of the URL */
|
||||
public queryParams: Params = {},
|
||||
/** The fragment of the URL */
|
||||
public fragment: string|null = null) {
|
||||
/** The root segment group of the URL tree */
|
||||
public root: UrlSegmentGroup = new UrlSegmentGroup([], {}),
|
||||
/** The query params of the URL */
|
||||
public queryParams: Params = {},
|
||||
/** The fragment of the URL */
|
||||
public fragment: string | null = null,
|
||||
) {
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
if (root.segments.length > 0) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROOT_URL_SEGMENT,
|
||||
'The root `UrlSegmentGroup` should not contain `segments`. ' +
|
||||
'Instead, these segments belong in the `children` so they can be associated with a named outlet.');
|
||||
RuntimeErrorCode.INVALID_ROOT_URL_SEGMENT,
|
||||
'The root `UrlSegmentGroup` should not contain `segments`. ' +
|
||||
'Instead, these segments belong in the `children` so they can be associated with a named outlet.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -229,13 +251,14 @@ export class UrlTree {
|
|||
*/
|
||||
export class UrlSegmentGroup {
|
||||
/** The parent node in the url tree */
|
||||
parent: UrlSegmentGroup|null = null;
|
||||
parent: UrlSegmentGroup | null = null;
|
||||
|
||||
constructor(
|
||||
/** The URL segments of this group. See `UrlSegment` for more information */
|
||||
public segments: UrlSegment[],
|
||||
/** The list of children of this group */
|
||||
public children: {[key: string]: UrlSegmentGroup}) {
|
||||
/** The URL segments of this group. See `UrlSegment` for more information */
|
||||
public segments: UrlSegment[],
|
||||
/** The list of children of this group */
|
||||
public children: {[key: string]: UrlSegmentGroup},
|
||||
) {
|
||||
Object.values(children).forEach((v) => (v.parent = this));
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +278,6 @@ export class UrlSegmentGroup {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
|
|
@ -287,11 +309,12 @@ export class UrlSegment {
|
|||
_parameterMap?: ParamMap;
|
||||
|
||||
constructor(
|
||||
/** The path part of a URL segment */
|
||||
public path: string,
|
||||
/** The path part of a URL segment */
|
||||
public path: string,
|
||||
|
||||
/** The matrix parameters associated with a segment */
|
||||
public parameters: {[name: string]: string}) {}
|
||||
/** The matrix parameters associated with a segment */
|
||||
public parameters: {[name: string]: string},
|
||||
) {}
|
||||
|
||||
get parameterMap(): ParamMap {
|
||||
this._parameterMap ??= convertToParamMap(this.parameters);
|
||||
|
|
@ -314,7 +337,9 @@ export function equalPath(as: UrlSegment[], bs: UrlSegment[]): boolean {
|
|||
}
|
||||
|
||||
export function mapChildrenIntoArray<T>(
|
||||
segment: UrlSegmentGroup, fn: (v: UrlSegmentGroup, k: string) => T[]): T[] {
|
||||
segment: UrlSegmentGroup,
|
||||
fn: (v: UrlSegmentGroup, k: string) => T[],
|
||||
): T[] {
|
||||
let res: T[] = [];
|
||||
Object.entries(segment.children).forEach(([childOutlet, child]) => {
|
||||
if (childOutlet === PRIMARY_OUTLET) {
|
||||
|
|
@ -329,7 +354,6 @@ export function mapChildrenIntoArray<T>(
|
|||
return res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
|
|
@ -381,7 +405,7 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
|||
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||
const query = serializeQueryParams(tree.queryParams);
|
||||
const fragment =
|
||||
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : '';
|
||||
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : '';
|
||||
|
||||
return `${segment}${query}${fragment}`;
|
||||
}
|
||||
|
|
@ -390,7 +414,7 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
|||
const DEFAULT_SERIALIZER = new DefaultUrlSerializer();
|
||||
|
||||
export function serializePaths(segment: UrlSegmentGroup): string {
|
||||
return segment.segments.map(p => serializePath(p)).join('/');
|
||||
return segment.segments.map((p) => serializePath(p)).join('/');
|
||||
}
|
||||
|
||||
function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||
|
|
@ -399,9 +423,9 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
|||
}
|
||||
|
||||
if (root) {
|
||||
const primary = segment.children[PRIMARY_OUTLET] ?
|
||||
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
|
||||
'';
|
||||
const primary = segment.children[PRIMARY_OUTLET]
|
||||
? serializeSegment(segment.children[PRIMARY_OUTLET], false)
|
||||
: '';
|
||||
const children: string[] = [];
|
||||
|
||||
Object.entries(segment.children).forEach(([k, v]) => {
|
||||
|
|
@ -411,7 +435,6 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
|||
});
|
||||
|
||||
return children.length > 0 ? `${primary}(${children.join('//')})` : primary;
|
||||
|
||||
} else {
|
||||
const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => {
|
||||
if (k === PRIMARY_OUTLET) {
|
||||
|
|
@ -438,10 +461,10 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
|||
*/
|
||||
function encodeUriString(s: string): string {
|
||||
return encodeURIComponent(s)
|
||||
.replace(/%40/g, '@')
|
||||
.replace(/%3A/gi, ':')
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',');
|
||||
.replace(/%40/g, '@')
|
||||
.replace(/%3A/gi, ':')
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -491,19 +514,18 @@ export function serializePath(path: UrlSegment): string {
|
|||
|
||||
function serializeMatrixParams(params: {[key: string]: string}): string {
|
||||
return Object.entries(params)
|
||||
.map(([key, value]) => `;${encodeUriSegment(key)}=${encodeUriSegment(value)}`)
|
||||
.join('');
|
||||
.map(([key, value]) => `;${encodeUriSegment(key)}=${encodeUriSegment(value)}`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function serializeQueryParams(params: {[key: string]: any}): string {
|
||||
const strParams: string[] =
|
||||
Object.entries(params)
|
||||
.map(([name, value]) => {
|
||||
return Array.isArray(value) ?
|
||||
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
|
||||
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
|
||||
})
|
||||
.filter(s => s);
|
||||
const strParams: string[] = Object.entries(params)
|
||||
.map(([name, value]) => {
|
||||
return Array.isArray(value)
|
||||
? value.map((v) => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&')
|
||||
: `${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
|
||||
})
|
||||
.filter((s) => s);
|
||||
|
||||
return strParams.length ? `?${strParams.join('&')}` : '';
|
||||
}
|
||||
|
|
@ -562,7 +584,7 @@ class UrlParser {
|
|||
return params;
|
||||
}
|
||||
|
||||
parseFragment(): string|null {
|
||||
parseFragment(): string | null {
|
||||
return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null;
|
||||
}
|
||||
|
||||
|
|
@ -607,9 +629,10 @@ class UrlParser {
|
|||
const path = matchSegments(this.remaining);
|
||||
if (path === '' && this.peekStartsWith(';')) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.EMPTY_PATH_WITH_PARAMS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Empty path url segment cannot have parameters: '${this.remaining}'.`);
|
||||
RuntimeErrorCode.EMPTY_PATH_WITH_PARAMS,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
`Empty path url segment cannot have parameters: '${this.remaining}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.capture(path);
|
||||
|
|
@ -689,8 +712,9 @@ class UrlParser {
|
|||
// or the group was not closed
|
||||
if (next !== '/' && next !== ')' && next !== ';') {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.UNPARSABLE_URL,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && `Cannot parse url '${this.url}'`);
|
||||
RuntimeErrorCode.UNPARSABLE_URL,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && `Cannot parse url '${this.url}'`,
|
||||
);
|
||||
}
|
||||
|
||||
let outletName: string = undefined!;
|
||||
|
|
@ -703,8 +727,10 @@ class UrlParser {
|
|||
}
|
||||
|
||||
const children = this.parseChildren();
|
||||
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
|
||||
new UrlSegmentGroup([], children);
|
||||
segments[outletName] =
|
||||
Object.keys(children).length === 1
|
||||
? children[PRIMARY_OUTLET]
|
||||
: new UrlSegmentGroup([], children);
|
||||
this.consumeOptional('//');
|
||||
}
|
||||
|
||||
|
|
@ -727,16 +753,17 @@ class UrlParser {
|
|||
private capture(str: string): void {
|
||||
if (!this.consumeOptional(str)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.UNEXPECTED_VALUE_IN_URL,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && `Expected "${str}".`);
|
||||
RuntimeErrorCode.UNEXPECTED_VALUE_IN_URL,
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && `Expected "${str}".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createRoot(rootCandidate: UrlSegmentGroup) {
|
||||
return rootCandidate.segments.length > 0 ?
|
||||
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
|
||||
rootCandidate;
|
||||
return rootCandidate.segments.length > 0
|
||||
? new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate})
|
||||
: rootCandidate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -754,12 +781,15 @@ export function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGro
|
|||
for (const [childOutlet, child] of Object.entries(segmentGroup.children)) {
|
||||
const childCandidate = squashSegmentGroup(child);
|
||||
// moves named children in an empty path primary child into this group
|
||||
if (childOutlet === PRIMARY_OUTLET && childCandidate.segments.length === 0 &&
|
||||
childCandidate.hasChildren()) {
|
||||
if (
|
||||
childOutlet === PRIMARY_OUTLET &&
|
||||
childCandidate.segments.length === 0 &&
|
||||
childCandidate.hasChildren()
|
||||
) {
|
||||
for (const [grandChildOutlet, grandChild] of Object.entries(childCandidate.children)) {
|
||||
newChildren[grandChildOutlet] = grandChild;
|
||||
}
|
||||
} // don't add empty children
|
||||
} // don't add empty children
|
||||
else if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) {
|
||||
newChildren[childOutlet] = childCandidate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export function shallowEqualArrays(a: any[], b: any[]): boolean {
|
|||
}
|
||||
|
||||
export function shallowEqual(
|
||||
a: {[key: string|symbol]: any}, b: {[key: string|symbol]: any}): boolean {
|
||||
a: {[key: string | symbol]: any},
|
||||
b: {[key: string | symbol]: any},
|
||||
): boolean {
|
||||
// While `undefined` should never be possible, it would sometimes be the case in IE 11
|
||||
// and pre-chromium Edge. The check below accounts for this edge case.
|
||||
const k1 = a ? getDataKeys(a) : undefined;
|
||||
|
|
@ -26,7 +28,7 @@ export function shallowEqual(
|
|||
if (!k1 || !k2 || k1.length != k2.length) {
|
||||
return false;
|
||||
}
|
||||
let key: string|symbol;
|
||||
let key: string | symbol;
|
||||
for (let i = 0; i < k1.length; i++) {
|
||||
key = k1[i];
|
||||
if (!equalArraysOrString(a[key], b[key])) {
|
||||
|
|
@ -39,14 +41,14 @@ export function shallowEqual(
|
|||
/**
|
||||
* Gets the keys of an object, including `symbol` keys.
|
||||
*/
|
||||
export function getDataKeys(obj: Object): Array<string|symbol> {
|
||||
export function getDataKeys(obj: Object): Array<string | symbol> {
|
||||
return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test equality for arrays of strings or a string.
|
||||
*/
|
||||
export function equalArraysOrString(a: string|string[], b: string|string[]) {
|
||||
export function equalArraysOrString(a: string | string[], b: string | string[]) {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
const aSorted = [...a].sort();
|
||||
|
|
@ -60,11 +62,11 @@ export function equalArraysOrString(a: string|string[], b: string|string[]) {
|
|||
/**
|
||||
* Return the last element of an array.
|
||||
*/
|
||||
export function last<T>(a: T[]): T|null {
|
||||
export function last<T>(a: T[]): T | null {
|
||||
return a.length > 0 ? a[a.length - 1] : null;
|
||||
}
|
||||
|
||||
export function wrapIntoObservable<T>(value: T|Promise<T>|Observable<T>): Observable<T> {
|
||||
export function wrapIntoObservable<T>(value: T | Promise<T> | Observable<T>): Observable<T> {
|
||||
if (isObservable(value)) {
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {createEnvironmentInjector, EnvironmentInjector, isStandalone, Type, ɵisNgModule as isNgModule, ɵRuntimeError as RuntimeError} from '@angular/core';
|
||||
import {
|
||||
createEnvironmentInjector,
|
||||
EnvironmentInjector,
|
||||
isStandalone,
|
||||
Type,
|
||||
ɵisNgModule as isNgModule,
|
||||
ɵRuntimeError as RuntimeError,
|
||||
} from '@angular/core';
|
||||
|
||||
import {EmptyOutletComponent} from '../components/empty_outlet';
|
||||
import {RuntimeErrorCode} from '../errors';
|
||||
|
|
@ -23,31 +30,39 @@ import {PRIMARY_OUTLET} from '../shared';
|
|||
* @param currentInjector The parent injector of the `Route`
|
||||
*/
|
||||
export function getOrCreateRouteInjectorIfNeeded(
|
||||
route: Route, currentInjector: EnvironmentInjector) {
|
||||
route: Route,
|
||||
currentInjector: EnvironmentInjector,
|
||||
) {
|
||||
if (route.providers && !route._injector) {
|
||||
route._injector =
|
||||
createEnvironmentInjector(route.providers, currentInjector, `Route: ${route.path}`);
|
||||
route._injector = createEnvironmentInjector(
|
||||
route.providers,
|
||||
currentInjector,
|
||||
`Route: ${route.path}`,
|
||||
);
|
||||
}
|
||||
return route._injector ?? currentInjector;
|
||||
}
|
||||
|
||||
export function getLoadedRoutes(route: Route): Route[]|undefined {
|
||||
export function getLoadedRoutes(route: Route): Route[] | undefined {
|
||||
return route._loadedRoutes;
|
||||
}
|
||||
|
||||
export function getLoadedInjector(route: Route): EnvironmentInjector|undefined {
|
||||
export function getLoadedInjector(route: Route): EnvironmentInjector | undefined {
|
||||
return route._loadedInjector;
|
||||
}
|
||||
export function getLoadedComponent(route: Route): Type<unknown>|undefined {
|
||||
export function getLoadedComponent(route: Route): Type<unknown> | undefined {
|
||||
return route._loadedComponent;
|
||||
}
|
||||
|
||||
export function getProvidersInjector(route: Route): EnvironmentInjector|undefined {
|
||||
export function getProvidersInjector(route: Route): EnvironmentInjector | undefined {
|
||||
return route._injector;
|
||||
}
|
||||
|
||||
export function validateConfig(
|
||||
config: Routes, parentPath: string = '', requireStandaloneComponents = false): void {
|
||||
config: Routes,
|
||||
parentPath: string = '',
|
||||
requireStandaloneComponents = false,
|
||||
): void {
|
||||
// forEach doesn't iterate undefined values
|
||||
for (let i = 0; i < config.length; i++) {
|
||||
const route: Route = config[i];
|
||||
|
|
@ -56,24 +71,27 @@ export function validateConfig(
|
|||
}
|
||||
}
|
||||
|
||||
export function assertStandalone(fullPath: string, component: Type<unknown>|undefined) {
|
||||
export function assertStandalone(fullPath: string, component: Type<unknown> | undefined) {
|
||||
if (component && isNgModule(component)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}'. You are using 'loadComponent' with a module, ` +
|
||||
`but it must be used with standalone components. Use 'loadChildren' instead.`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}'. You are using 'loadComponent' with a module, ` +
|
||||
`but it must be used with standalone components. Use 'loadChildren' instead.`,
|
||||
);
|
||||
} else if (component && !isStandalone(component)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}'. The component must be standalone.`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}'. The component must be standalone.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNode(route: Route, fullPath: string, requireStandaloneComponents: boolean): void {
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
if (!route) {
|
||||
throw new RuntimeError(RuntimeErrorCode.INVALID_ROUTE_CONFIG, `
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`
|
||||
Invalid configuration of route '${fullPath}': Encountered undefined route.
|
||||
The reason might be an extra comma.
|
||||
|
||||
|
|
@ -83,87 +101,102 @@ function validateNode(route: Route, fullPath: string, requireStandaloneComponent
|
|||
{ path: 'dashboard', component: DashboardComponent },, << two commas
|
||||
{ path: 'detail/:id', component: HeroDetailComponent }
|
||||
];
|
||||
`);
|
||||
`,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(route)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': Array cannot be specified`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': Array cannot be specified`,
|
||||
);
|
||||
}
|
||||
if (!route.redirectTo && !route.component && !route.loadComponent && !route.children &&
|
||||
!route.loadChildren && (route.outlet && route.outlet !== PRIMARY_OUTLET)) {
|
||||
if (
|
||||
!route.redirectTo &&
|
||||
!route.component &&
|
||||
!route.loadComponent &&
|
||||
!route.children &&
|
||||
!route.loadChildren &&
|
||||
route.outlet &&
|
||||
route.outlet !== PRIMARY_OUTLET
|
||||
) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`,
|
||||
);
|
||||
}
|
||||
if (route.redirectTo && route.children) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': redirectTo and children cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.redirectTo && route.loadChildren) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': redirectTo and loadChildren cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.children && route.loadChildren) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': children and loadChildren cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.redirectTo && (route.component || route.loadComponent)) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': redirectTo and component/loadComponent cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and component/loadComponent cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.component && route.loadComponent) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': component and loadComponent cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': component and loadComponent cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.redirectTo && route.canActivate) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': redirectTo and canActivate cannot be used together. Redirects happen before activation ` +
|
||||
`so canActivate will never be executed.`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and canActivate cannot be used together. Redirects happen before activation ` +
|
||||
`so canActivate will never be executed.`,
|
||||
);
|
||||
}
|
||||
if (route.path && route.matcher) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`,
|
||||
);
|
||||
}
|
||||
if (route.redirectTo === void 0 && !route.component && !route.loadComponent &&
|
||||
!route.children && !route.loadChildren) {
|
||||
if (
|
||||
route.redirectTo === void 0 &&
|
||||
!route.component &&
|
||||
!route.loadComponent &&
|
||||
!route.children &&
|
||||
!route.loadChildren
|
||||
) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`,
|
||||
);
|
||||
}
|
||||
if (route.path === void 0 && route.matcher === void 0) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${
|
||||
fullPath}': routes must have either a path or a matcher specified`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': routes must have either a path or a matcher specified`,
|
||||
);
|
||||
}
|
||||
if (typeof route.path === 'string' && route.path.charAt(0) === '/') {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': path cannot start with a slash`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '${fullPath}': path cannot start with a slash`,
|
||||
);
|
||||
}
|
||||
if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) {
|
||||
const exp =
|
||||
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
|
||||
const exp = `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${
|
||||
route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
|
||||
RuntimeErrorCode.INVALID_ROUTE_CONFIG,
|
||||
`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`,
|
||||
);
|
||||
}
|
||||
if (requireStandaloneComponents) {
|
||||
assertStandalone(fullPath, route.component);
|
||||
|
|
@ -195,8 +228,13 @@ function getFullPath(parentPath: string, currentRoute: Route): string {
|
|||
export function standardizeConfig(r: Route): Route {
|
||||
const children = r.children && r.children.map(standardizeConfig);
|
||||
const c = children ? {...r, children} : {...r};
|
||||
if ((!c.component && !c.loadComponent) && (children || c.loadChildren) &&
|
||||
(c.outlet && c.outlet !== PRIMARY_OUTLET)) {
|
||||
if (
|
||||
!c.component &&
|
||||
!c.loadComponent &&
|
||||
(children || c.loadChildren) &&
|
||||
c.outlet &&
|
||||
c.outlet !== PRIMARY_OUTLET
|
||||
) {
|
||||
c.component = EmptyOutletComponent;
|
||||
}
|
||||
return c;
|
||||
|
|
@ -212,8 +250,8 @@ export function getOutlet(route: Route): string {
|
|||
* The order of the configs is otherwise preserved.
|
||||
*/
|
||||
export function sortByMatchingOutlets(routes: Routes, outletName: string): Routes {
|
||||
const sortedConfig = routes.filter(r => getOutlet(r) === outletName);
|
||||
sortedConfig.push(...routes.filter(r => getOutlet(r) !== outletName));
|
||||
const sortedConfig = routes.filter((r) => getOutlet(r) === outletName);
|
||||
sortedConfig.push(...routes.filter((r) => getOutlet(r) !== outletName));
|
||||
return sortedConfig;
|
||||
}
|
||||
|
||||
|
|
@ -229,8 +267,9 @@ export function sortByMatchingOutlets(routes: Routes, outletName: string): Route
|
|||
* Generally used for retrieving the injector to use for getting tokens for guards/resolvers and
|
||||
* also used for getting the correct injector to use for creating components.
|
||||
*/
|
||||
export function getClosestRouteInjector(snapshot: ActivatedRouteSnapshot): EnvironmentInjector|
|
||||
null {
|
||||
export function getClosestRouteInjector(
|
||||
snapshot: ActivatedRouteSnapshot,
|
||||
): EnvironmentInjector | null {
|
||||
if (!snapshot) return null;
|
||||
|
||||
// If the current route has its own injector, which is created from the static providers on the
|
||||
|
|
|
|||
|
|
@ -31,12 +31,16 @@ const noMatch: MatchResult = {
|
|||
consumedSegments: [],
|
||||
remainingSegments: [],
|
||||
parameters: {},
|
||||
positionalParamSegments: {}
|
||||
positionalParamSegments: {},
|
||||
};
|
||||
|
||||
export function matchWithChecks(
|
||||
segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[],
|
||||
injector: EnvironmentInjector, urlSerializer: UrlSerializer): Observable<MatchResult> {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
injector: EnvironmentInjector,
|
||||
urlSerializer: UrlSerializer,
|
||||
): Observable<MatchResult> {
|
||||
const result = match(segmentGroup, route, segments);
|
||||
if (!result.matched) {
|
||||
return of(result);
|
||||
|
|
@ -45,14 +49,16 @@ export function matchWithChecks(
|
|||
// Only create the Route's `EnvironmentInjector` if it matches the attempted
|
||||
// navigation
|
||||
injector = getOrCreateRouteInjectorIfNeeded(route, injector);
|
||||
return runCanMatchGuards(injector, route, segments, urlSerializer)
|
||||
.pipe(
|
||||
map((v) => v === true ? result : {...noMatch}),
|
||||
);
|
||||
return runCanMatchGuards(injector, route, segments, urlSerializer).pipe(
|
||||
map((v) => (v === true ? result : {...noMatch})),
|
||||
);
|
||||
}
|
||||
|
||||
export function match(
|
||||
segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
route: Route,
|
||||
segments: UrlSegment[],
|
||||
): MatchResult {
|
||||
if (route.path === '**') {
|
||||
return createWildcardMatchResult(segments);
|
||||
}
|
||||
|
|
@ -67,7 +73,7 @@ export function match(
|
|||
consumedSegments: [],
|
||||
remainingSegments: segments,
|
||||
parameters: {},
|
||||
positionalParamSegments: {}
|
||||
positionalParamSegments: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +85,10 @@ export function match(
|
|||
Object.entries(res.posParams ?? {}).forEach(([k, v]) => {
|
||||
posParams[k] = v.path;
|
||||
});
|
||||
const parameters = res.consumed.length > 0 ?
|
||||
{...posParams, ...res.consumed[res.consumed.length - 1].parameters} :
|
||||
posParams;
|
||||
const parameters =
|
||||
res.consumed.length > 0
|
||||
? {...posParams, ...res.consumed[res.consumed.length - 1].parameters}
|
||||
: posParams;
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
|
|
@ -89,7 +96,7 @@ export function match(
|
|||
remainingSegments: segments.slice(res.consumed.length),
|
||||
// TODO(atscott): investigate combining parameters and positionalParamSegments
|
||||
parameters,
|
||||
positionalParamSegments: res.posParams ?? {}
|
||||
positionalParamSegments: res.posParams ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -104,23 +111,33 @@ function createWildcardMatchResult(segments: UrlSegment[]): MatchResult {
|
|||
}
|
||||
|
||||
export function split(
|
||||
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
||||
config: Route[]) {
|
||||
if (slicedSegments.length > 0 &&
|
||||
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
consumedSegments: UrlSegment[],
|
||||
slicedSegments: UrlSegment[],
|
||||
config: Route[],
|
||||
) {
|
||||
if (
|
||||
slicedSegments.length > 0 &&
|
||||
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)
|
||||
) {
|
||||
const s = new UrlSegmentGroup(
|
||||
consumedSegments,
|
||||
createChildrenForEmptyPaths(
|
||||
config, new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
|
||||
consumedSegments,
|
||||
createChildrenForEmptyPaths(
|
||||
config,
|
||||
new UrlSegmentGroup(slicedSegments, segmentGroup.children),
|
||||
),
|
||||
);
|
||||
return {segmentGroup: s, slicedSegments: []};
|
||||
}
|
||||
|
||||
if (slicedSegments.length === 0 &&
|
||||
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
|
||||
if (
|
||||
slicedSegments.length === 0 &&
|
||||
containsEmptyPathMatches(segmentGroup, slicedSegments, config)
|
||||
) {
|
||||
const s = new UrlSegmentGroup(
|
||||
segmentGroup.segments,
|
||||
addEmptyPathsToChildrenIfNeeded(
|
||||
segmentGroup, slicedSegments, config, segmentGroup.children));
|
||||
segmentGroup.segments,
|
||||
addEmptyPathsToChildrenIfNeeded(segmentGroup, slicedSegments, config, segmentGroup.children),
|
||||
);
|
||||
return {segmentGroup: s, slicedSegments};
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +146,11 @@ export function split(
|
|||
}
|
||||
|
||||
function addEmptyPathsToChildrenIfNeeded(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
|
||||
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
slicedSegments: UrlSegment[],
|
||||
routes: Route[],
|
||||
children: {[name: string]: UrlSegmentGroup},
|
||||
): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
for (const r of routes) {
|
||||
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
||||
|
|
@ -142,7 +162,9 @@ function addEmptyPathsToChildrenIfNeeded(
|
|||
}
|
||||
|
||||
function createChildrenForEmptyPaths(
|
||||
routes: Route[], primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
|
||||
routes: Route[],
|
||||
primarySegment: UrlSegmentGroup,
|
||||
): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
res[PRIMARY_OUTLET] = primarySegment;
|
||||
|
||||
|
|
@ -156,18 +178,28 @@ function createChildrenForEmptyPaths(
|
|||
}
|
||||
|
||||
function containsEmptyPathMatchesWithNamedOutlets(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
slicedSegments: UrlSegment[],
|
||||
routes: Route[],
|
||||
): boolean {
|
||||
return routes.some(
|
||||
r => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET);
|
||||
(r) => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET,
|
||||
);
|
||||
}
|
||||
|
||||
function containsEmptyPathMatches(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
return routes.some(r => emptyPathMatch(segmentGroup, slicedSegments, r));
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
slicedSegments: UrlSegment[],
|
||||
routes: Route[],
|
||||
): boolean {
|
||||
return routes.some((r) => emptyPathMatch(segmentGroup, slicedSegments, r));
|
||||
}
|
||||
|
||||
function emptyPathMatch(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
slicedSegments: UrlSegment[],
|
||||
r: Route,
|
||||
): boolean {
|
||||
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -181,7 +213,11 @@ function emptyPathMatch(
|
|||
* well.
|
||||
*/
|
||||
export function isImmediateMatch(
|
||||
route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean {
|
||||
route: Route,
|
||||
rawSegment: UrlSegmentGroup,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
): boolean {
|
||||
// We allow matches to empty paths when the outlets differ so we can match a url like `/(b:b)` to
|
||||
// a config like
|
||||
// * `{path: '', children: [{path: 'b', outlet: 'b'}]}`
|
||||
|
|
@ -193,14 +229,19 @@ export function isImmediateMatch(
|
|||
// outlets. So we need to prevent child named outlet matches in a url like `/b` in a config like
|
||||
// * `{path: '', outlet: 'x' children: [{path: 'b'}]}`
|
||||
// This should only match if the url is `/(x:b)`.
|
||||
if (getOutlet(route) !== outlet &&
|
||||
(outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route))) {
|
||||
if (
|
||||
getOutlet(route) !== outlet &&
|
||||
(outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return match(rawSegment, route, segments).matched;
|
||||
}
|
||||
|
||||
export function noLeftoversInUrl(
|
||||
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean {
|
||||
segmentGroup: UrlSegmentGroup,
|
||||
segments: UrlSegment[],
|
||||
outlet: string,
|
||||
): boolean {
|
||||
return segments.length === 0 && !segmentGroup.children[outlet];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanMatchFn, ResolveF
|
|||
* @see {@link Route}
|
||||
*/
|
||||
export function mapToCanMatch(providers: Array<Type<{canMatch: CanMatchFn}>>): CanMatchFn[] {
|
||||
return providers.map(provider => (...params) => inject(provider).canMatch(...params));
|
||||
return providers.map(
|
||||
(provider) =>
|
||||
(...params) =>
|
||||
inject(provider).canMatch(...params),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,9 +36,14 @@ export function mapToCanMatch(providers: Array<Type<{canMatch: CanMatchFn}>>): C
|
|||
* @publicApi
|
||||
* @see {@link Route}
|
||||
*/
|
||||
export function mapToCanActivate(providers: Array<Type<{canActivate: CanActivateFn}>>):
|
||||
CanActivateFn[] {
|
||||
return providers.map(provider => (...params) => inject(provider).canActivate(...params));
|
||||
export function mapToCanActivate(
|
||||
providers: Array<Type<{canActivate: CanActivateFn}>>,
|
||||
): CanActivateFn[] {
|
||||
return providers.map(
|
||||
(provider) =>
|
||||
(...params) =>
|
||||
inject(provider).canActivate(...params),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Maps an array of injectable classes with canActivateChild functions to an array of equivalent
|
||||
|
|
@ -46,8 +55,13 @@ export function mapToCanActivate(providers: Array<Type<{canActivate: CanActivate
|
|||
* @see {@link Route}
|
||||
*/
|
||||
export function mapToCanActivateChild(
|
||||
providers: Array<Type<{canActivateChild: CanActivateChildFn}>>): CanActivateChildFn[] {
|
||||
return providers.map(provider => (...params) => inject(provider).canActivateChild(...params));
|
||||
providers: Array<Type<{canActivateChild: CanActivateChildFn}>>,
|
||||
): CanActivateChildFn[] {
|
||||
return providers.map(
|
||||
(provider) =>
|
||||
(...params) =>
|
||||
inject(provider).canActivateChild(...params),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Maps an array of injectable classes with canDeactivate functions to an array of equivalent
|
||||
|
|
@ -59,8 +73,13 @@ export function mapToCanActivateChild(
|
|||
* @see {@link Route}
|
||||
*/
|
||||
export function mapToCanDeactivate<T = unknown>(
|
||||
providers: Array<Type<{canDeactivate: CanDeactivateFn<T>}>>): CanDeactivateFn<T>[] {
|
||||
return providers.map(provider => (...params) => inject(provider).canDeactivate(...params));
|
||||
providers: Array<Type<{canDeactivate: CanDeactivateFn<T>}>>,
|
||||
): CanDeactivateFn<T>[] {
|
||||
return providers.map(
|
||||
(provider) =>
|
||||
(...params) =>
|
||||
inject(provider).canDeactivate(...params),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Maps an injectable class with a resolve function to an equivalent `ResolveFn`
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@
|
|||
import {Observable} from 'rxjs';
|
||||
import {filter, map, take} from 'rxjs/operators';
|
||||
|
||||
import {Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped} from '../events';
|
||||
import {
|
||||
Event,
|
||||
NavigationCancel,
|
||||
NavigationCancellationCode,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationSkipped,
|
||||
} from '../events';
|
||||
|
||||
enum NavigationResult {
|
||||
COMPLETE,
|
||||
|
|
@ -28,27 +35,32 @@ enum NavigationResult {
|
|||
*/
|
||||
export function afterNextNavigation(router: {events: Observable<Event>}, action: () => void) {
|
||||
router.events
|
||||
.pipe(
|
||||
filter(
|
||||
(e): e is NavigationEnd|NavigationCancel|NavigationError|NavigationSkipped =>
|
||||
e instanceof NavigationEnd || e instanceof NavigationCancel ||
|
||||
e instanceof NavigationError || e instanceof NavigationSkipped),
|
||||
map(e => {
|
||||
if (e instanceof NavigationEnd || e instanceof NavigationSkipped) {
|
||||
return NavigationResult.COMPLETE;
|
||||
}
|
||||
const redirecting = e instanceof NavigationCancel ?
|
||||
(e.code === NavigationCancellationCode.Redirect ||
|
||||
e.code === NavigationCancellationCode.SupersededByNewNavigation) :
|
||||
false;
|
||||
return redirecting ? NavigationResult.REDIRECTING : NavigationResult.FAILED;
|
||||
}),
|
||||
filter(
|
||||
(result): result is NavigationResult.COMPLETE|NavigationResult.FAILED =>
|
||||
result !== NavigationResult.REDIRECTING),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(() => {
|
||||
action();
|
||||
});
|
||||
.pipe(
|
||||
filter(
|
||||
(e): e is NavigationEnd | NavigationCancel | NavigationError | NavigationSkipped =>
|
||||
e instanceof NavigationEnd ||
|
||||
e instanceof NavigationCancel ||
|
||||
e instanceof NavigationError ||
|
||||
e instanceof NavigationSkipped,
|
||||
),
|
||||
map((e) => {
|
||||
if (e instanceof NavigationEnd || e instanceof NavigationSkipped) {
|
||||
return NavigationResult.COMPLETE;
|
||||
}
|
||||
const redirecting =
|
||||
e instanceof NavigationCancel
|
||||
? e.code === NavigationCancellationCode.Redirect ||
|
||||
e.code === NavigationCancellationCode.SupersededByNewNavigation
|
||||
: false;
|
||||
return redirecting ? NavigationResult.REDIRECTING : NavigationResult.FAILED;
|
||||
}),
|
||||
filter(
|
||||
(result): result is NavigationResult.COMPLETE | NavigationResult.FAILED =>
|
||||
result !== NavigationResult.REDIRECTING,
|
||||
),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(() => {
|
||||
action();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import {Injector, ProviderToken, ɵisInjectable as isInjectable} from '@angular/
|
|||
|
||||
import {RunGuardsAndResolvers} from '../models';
|
||||
import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context';
|
||||
import {ActivatedRouteSnapshot, equalParamsAndUrlSegments, RouterStateSnapshot} from '../router_state';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
equalParamsAndUrlSegments,
|
||||
RouterStateSnapshot,
|
||||
} from '../router_state';
|
||||
import {equalPath} from '../url_tree';
|
||||
import {shallowEqual} from '../utils/collection';
|
||||
import {nodeChildrenAsMap, TreeNode} from '../utils/tree';
|
||||
|
|
@ -23,34 +27,42 @@ export class CanActivate {
|
|||
}
|
||||
|
||||
export class CanDeactivate {
|
||||
constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {}
|
||||
constructor(
|
||||
public component: Object | null,
|
||||
public route: ActivatedRouteSnapshot,
|
||||
) {}
|
||||
}
|
||||
|
||||
export declare type Checks = {
|
||||
canDeactivateChecks: CanDeactivate[],
|
||||
canActivateChecks: CanActivate[],
|
||||
canDeactivateChecks: CanDeactivate[];
|
||||
canActivateChecks: CanActivate[];
|
||||
};
|
||||
|
||||
export function getAllRouteGuards(
|
||||
future: RouterStateSnapshot, curr: RouterStateSnapshot,
|
||||
parentContexts: ChildrenOutletContexts) {
|
||||
future: RouterStateSnapshot,
|
||||
curr: RouterStateSnapshot,
|
||||
parentContexts: ChildrenOutletContexts,
|
||||
) {
|
||||
const futureRoot = future._root;
|
||||
const currRoot = curr ? curr._root : null;
|
||||
|
||||
return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]);
|
||||
}
|
||||
|
||||
export function getCanActivateChild(p: ActivatedRouteSnapshot):
|
||||
{node: ActivatedRouteSnapshot, guards: any[]}|null {
|
||||
export function getCanActivateChild(
|
||||
p: ActivatedRouteSnapshot,
|
||||
): {node: ActivatedRouteSnapshot; guards: any[]} | null {
|
||||
const canActivateChild = p.routeConfig ? p.routeConfig.canActivateChild : null;
|
||||
if (!canActivateChild || canActivateChild.length === 0) return null;
|
||||
return {node: p, guards: canActivateChild};
|
||||
}
|
||||
|
||||
export function getTokenOrFunctionIdentity<T>(
|
||||
tokenOrFunction: Function|ProviderToken<T>, injector: Injector): Function|T {
|
||||
tokenOrFunction: Function | ProviderToken<T>,
|
||||
injector: Injector,
|
||||
): Function | T {
|
||||
const NOT_FOUND = Symbol();
|
||||
const result = injector.get<T|Symbol>(tokenOrFunction, NOT_FOUND);
|
||||
const result = injector.get<T | Symbol>(tokenOrFunction, NOT_FOUND);
|
||||
if (result === NOT_FOUND) {
|
||||
if (typeof tokenOrFunction === 'function' && !isInjectable(tokenOrFunction)) {
|
||||
// We think the token is just a function so return it as-is
|
||||
|
|
@ -64,43 +76,52 @@ export function getTokenOrFunctionIdentity<T>(
|
|||
}
|
||||
|
||||
function getChildRouteGuards(
|
||||
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>|null,
|
||||
contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[], checks: Checks = {
|
||||
canDeactivateChecks: [],
|
||||
canActivateChecks: []
|
||||
}): Checks {
|
||||
futureNode: TreeNode<ActivatedRouteSnapshot>,
|
||||
currNode: TreeNode<ActivatedRouteSnapshot> | null,
|
||||
contexts: ChildrenOutletContexts | null,
|
||||
futurePath: ActivatedRouteSnapshot[],
|
||||
checks: Checks = {
|
||||
canDeactivateChecks: [],
|
||||
canActivateChecks: [],
|
||||
},
|
||||
): Checks {
|
||||
const prevChildren = nodeChildrenAsMap(currNode);
|
||||
|
||||
// Process the children of the future route
|
||||
futureNode.children.forEach(c => {
|
||||
futureNode.children.forEach((c) => {
|
||||
getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks);
|
||||
delete prevChildren[c.value.outlet];
|
||||
});
|
||||
|
||||
// Process any children left from the current route (not active for the future route)
|
||||
Object.entries(prevChildren)
|
||||
.forEach(
|
||||
([k, v]: [string, TreeNode<ActivatedRouteSnapshot>]) =>
|
||||
deactivateRouteAndItsChildren(v, contexts!.getContext(k), checks));
|
||||
Object.entries(prevChildren).forEach(([k, v]: [string, TreeNode<ActivatedRouteSnapshot>]) =>
|
||||
deactivateRouteAndItsChildren(v, contexts!.getContext(k), checks),
|
||||
);
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
function getRouteGuards(
|
||||
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
|
||||
parentContexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[],
|
||||
checks: Checks = {
|
||||
canDeactivateChecks: [],
|
||||
canActivateChecks: []
|
||||
}): Checks {
|
||||
futureNode: TreeNode<ActivatedRouteSnapshot>,
|
||||
currNode: TreeNode<ActivatedRouteSnapshot>,
|
||||
parentContexts: ChildrenOutletContexts | null,
|
||||
futurePath: ActivatedRouteSnapshot[],
|
||||
checks: Checks = {
|
||||
canDeactivateChecks: [],
|
||||
canActivateChecks: [],
|
||||
},
|
||||
): Checks {
|
||||
const future = futureNode.value;
|
||||
const curr = currNode ? currNode.value : null;
|
||||
const context = parentContexts ? parentContexts.getContext(futureNode.value.outlet) : null;
|
||||
|
||||
// reusing the node
|
||||
if (curr && future.routeConfig === curr.routeConfig) {
|
||||
const shouldRun =
|
||||
shouldRunGuardsAndResolvers(curr, future, future.routeConfig!.runGuardsAndResolvers);
|
||||
const shouldRun = shouldRunGuardsAndResolvers(
|
||||
curr,
|
||||
future,
|
||||
future.routeConfig!.runGuardsAndResolvers,
|
||||
);
|
||||
if (shouldRun) {
|
||||
checks.canActivateChecks.push(new CanActivate(futurePath));
|
||||
} else {
|
||||
|
|
@ -112,7 +133,12 @@ function getRouteGuards(
|
|||
// If we have a component, we need to go through an outlet.
|
||||
if (future.component) {
|
||||
getChildRouteGuards(
|
||||
futureNode, currNode, context ? context.children : null, futurePath, checks);
|
||||
futureNode,
|
||||
currNode,
|
||||
context ? context.children : null,
|
||||
futurePath,
|
||||
checks,
|
||||
);
|
||||
|
||||
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||
} else {
|
||||
|
|
@ -142,8 +168,10 @@ function getRouteGuards(
|
|||
}
|
||||
|
||||
function shouldRunGuardsAndResolvers(
|
||||
curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot,
|
||||
mode: RunGuardsAndResolvers|undefined): boolean {
|
||||
curr: ActivatedRouteSnapshot,
|
||||
future: ActivatedRouteSnapshot,
|
||||
mode: RunGuardsAndResolvers | undefined,
|
||||
): boolean {
|
||||
if (typeof mode === 'function') {
|
||||
return mode(curr, future);
|
||||
}
|
||||
|
|
@ -152,15 +180,18 @@ function shouldRunGuardsAndResolvers(
|
|||
return !equalPath(curr.url, future.url);
|
||||
|
||||
case 'pathParamsOrQueryParamsChange':
|
||||
return !equalPath(curr.url, future.url) ||
|
||||
!shallowEqual(curr.queryParams, future.queryParams);
|
||||
return (
|
||||
!equalPath(curr.url, future.url) || !shallowEqual(curr.queryParams, future.queryParams)
|
||||
);
|
||||
|
||||
case 'always':
|
||||
return true;
|
||||
|
||||
case 'paramsOrQueryParamsChange':
|
||||
return !equalParamsAndUrlSegments(curr, future) ||
|
||||
!shallowEqual(curr.queryParams, future.queryParams);
|
||||
return (
|
||||
!equalParamsAndUrlSegments(curr, future) ||
|
||||
!shallowEqual(curr.queryParams, future.queryParams)
|
||||
);
|
||||
|
||||
case 'paramsChange':
|
||||
default:
|
||||
|
|
@ -169,7 +200,10 @@ function shouldRunGuardsAndResolvers(
|
|||
}
|
||||
|
||||
function deactivateRouteAndItsChildren(
|
||||
route: TreeNode<ActivatedRouteSnapshot>, context: OutletContext|null, checks: Checks): void {
|
||||
route: TreeNode<ActivatedRouteSnapshot>,
|
||||
context: OutletContext | null,
|
||||
checks: Checks,
|
||||
): void {
|
||||
const children = nodeChildrenAsMap(route);
|
||||
const r = route.value;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class Tree<T> {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
parent(t: T): T|null {
|
||||
parent(t: T): T | null {
|
||||
const p = this.pathFromRoot(t);
|
||||
return p.length > 1 ? p[p.length - 2] : null;
|
||||
}
|
||||
|
|
@ -31,13 +31,13 @@ export class Tree<T> {
|
|||
*/
|
||||
children(t: T): T[] {
|
||||
const n = findNode(t, this._root);
|
||||
return n ? n.children.map(t => t.value) : [];
|
||||
return n ? n.children.map((t) => t.value) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
firstChild(t: T): T|null {
|
||||
firstChild(t: T): T | null {
|
||||
const n = findNode(t, this._root);
|
||||
return n && n.children.length > 0 ? n.children[0].value : null;
|
||||
}
|
||||
|
|
@ -49,21 +49,20 @@ export class Tree<T> {
|
|||
const p = findPath(t, this._root);
|
||||
if (p.length < 2) return [];
|
||||
|
||||
const c = p[p.length - 2].children.map(c => c.value);
|
||||
return c.filter(cc => cc !== t);
|
||||
const c = p[p.length - 2].children.map((c) => c.value);
|
||||
return c.filter((cc) => cc !== t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
pathFromRoot(t: T): T[] {
|
||||
return findPath(t, this._root).map(s => s.value);
|
||||
return findPath(t, this._root).map((s) => s.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DFS for the node matching the value
|
||||
function findNode<T>(value: T, node: TreeNode<T>): TreeNode<T>|null {
|
||||
function findNode<T>(value: T, node: TreeNode<T>): TreeNode<T> | null {
|
||||
if (value === node.value) return node;
|
||||
|
||||
for (const child of node.children) {
|
||||
|
|
@ -90,7 +89,10 @@ function findPath<T>(value: T, node: TreeNode<T>): TreeNode<T>[] {
|
|||
}
|
||||
|
||||
export class TreeNode<T> {
|
||||
constructor(public value: T, public children: TreeNode<T>[]) {}
|
||||
constructor(
|
||||
public value: T,
|
||||
public children: TreeNode<T>[],
|
||||
) {}
|
||||
|
||||
toString(): string {
|
||||
return `TreeNode(${this.value})`;
|
||||
|
|
@ -98,11 +100,11 @@ export class TreeNode<T> {
|
|||
}
|
||||
|
||||
// Return the list of T indexed by outlet name
|
||||
export function nodeChildrenAsMap<T extends {outlet: string}>(node: TreeNode<T>|null) {
|
||||
export function nodeChildrenAsMap<T extends {outlet: string}>(node: TreeNode<T> | null) {
|
||||
const map: {[outlet: string]: TreeNode<T>} = {};
|
||||
|
||||
if (node) {
|
||||
node.children.forEach(child => map[child.value.outlet] = child);
|
||||
node.children.forEach((child) => (map[child.value.outlet] = child));
|
||||
}
|
||||
|
||||
return map;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@
|
|||
import {EmptyError} from 'rxjs';
|
||||
|
||||
import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn} from '../models';
|
||||
import {NAVIGATION_CANCELING_ERROR, NavigationCancelingError, RedirectingNavigationCancelingError} from '../navigation_canceling_error';
|
||||
import {
|
||||
NAVIGATION_CANCELING_ERROR,
|
||||
NavigationCancelingError,
|
||||
RedirectingNavigationCancelingError,
|
||||
} from '../navigation_canceling_error';
|
||||
import {isUrlTree} from '../url_tree';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,15 +9,22 @@
|
|||
/// <reference types="dom-view-transitions" />
|
||||
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {afterNextRender, InjectionToken, Injector, NgZone, runInInjectionContext} from '@angular/core';
|
||||
import {
|
||||
afterNextRender,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
NgZone,
|
||||
runInInjectionContext,
|
||||
} from '@angular/core';
|
||||
|
||||
import {ActivatedRouteSnapshot} from '../router_state';
|
||||
|
||||
export const CREATE_VIEW_TRANSITION =
|
||||
new InjectionToken<typeof createViewTransition>(ngDevMode ? 'view transition helper' : '');
|
||||
export const VIEW_TRANSITION_OPTIONS =
|
||||
new InjectionToken<ViewTransitionsFeatureOptions&{skipNextTransition: boolean}>(
|
||||
ngDevMode ? 'view transition options' : '');
|
||||
export const CREATE_VIEW_TRANSITION = new InjectionToken<typeof createViewTransition>(
|
||||
ngDevMode ? 'view transition helper' : '',
|
||||
);
|
||||
export const VIEW_TRANSITION_OPTIONS = new InjectionToken<
|
||||
ViewTransitionsFeatureOptions & {skipNextTransition: boolean}
|
||||
>(ngDevMode ? 'view transition options' : '');
|
||||
|
||||
/**
|
||||
* Options to configure the View Transitions integration in the Router.
|
||||
|
|
@ -60,19 +67,19 @@ export interface ViewTransitionInfo {
|
|||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/finished
|
||||
*/
|
||||
finished: Promise<void>,
|
||||
finished: Promise<void>;
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/ready
|
||||
*/
|
||||
ready: Promise<void>,
|
||||
ready: Promise<void>;
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/updateCallbackDone
|
||||
*/
|
||||
updateCallbackDone: Promise<void>,
|
||||
updateCallbackDone: Promise<void>;
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/skipTransition
|
||||
*/
|
||||
skipTransition(): void,
|
||||
skipTransition(): void;
|
||||
};
|
||||
/**
|
||||
* The `ActivatedRouteSnapshot` that the navigation is transitioning from.
|
||||
|
|
@ -91,7 +98,10 @@ export interface ViewTransitionInfo {
|
|||
* @returns A Promise that resolves when the view transition callback begins.
|
||||
*/
|
||||
export function createViewTransition(
|
||||
injector: Injector, from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot): Promise<void> {
|
||||
injector: Injector,
|
||||
from: ActivatedRouteSnapshot,
|
||||
to: ActivatedRouteSnapshot,
|
||||
): Promise<void> {
|
||||
const transitionOptions = injector.get(VIEW_TRANSITION_OPTIONS);
|
||||
const document = injector.get(DOCUMENT);
|
||||
// Create promises outside the Angular zone to avoid causing extra change detections
|
||||
|
|
@ -125,7 +135,7 @@ export function createViewTransition(
|
|||
* Creates a promise that resolves after next render.
|
||||
*/
|
||||
function createRenderPromise(injector: Injector) {
|
||||
return new Promise<void>(resolve => {
|
||||
return new Promise<void>((resolve) => {
|
||||
afterNextRender(resolve, {injector});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,11 +10,28 @@ import {DOCUMENT, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common';
|
|||
import {BrowserPlatformLocation} from '@angular/common/src/location/platform_location';
|
||||
import {NullViewportScroller, ViewportScroller} from '@angular/common/src/viewport_scroller';
|
||||
import {MockPlatformLocation} from '@angular/common/testing';
|
||||
import {ApplicationRef, Component, CUSTOM_ELEMENTS_SCHEMA, destroyPlatform, ENVIRONMENT_INITIALIZER, inject, Injectable, NgModule} from '@angular/core';
|
||||
import {
|
||||
ApplicationRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
destroyPlatform,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
inject,
|
||||
Injectable,
|
||||
NgModule,
|
||||
} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {Event, NavigationEnd, provideRouter, Router, RouterModule, RouterOutlet, withEnabledBlockingInitialNavigation} from '@angular/router';
|
||||
import {
|
||||
Event,
|
||||
NavigationEnd,
|
||||
provideRouter,
|
||||
Router,
|
||||
RouterModule,
|
||||
RouterOutlet,
|
||||
withEnabledBlockingInitialNavigation,
|
||||
} from '@angular/router';
|
||||
|
||||
// This is needed, because all files under `packages/` are compiled together as part of the
|
||||
// [legacy-unit-tests-saucelabs][1] CI job, including the `lib.webworker.d.ts` typings brought in by
|
||||
|
|
@ -31,8 +48,7 @@ describe('bootstrap', () => {
|
|||
let testProviders: any[] = null!;
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
class SimpleCmp {}
|
||||
|
||||
@Component({selector: 'test-app', template: 'root <router-outlet></router-outlet>'})
|
||||
class RootCmp {
|
||||
|
|
@ -42,14 +58,13 @@ describe('bootstrap', () => {
|
|||
}
|
||||
|
||||
@Component({selector: 'test-app2', template: 'root <router-outlet></router-outlet>'})
|
||||
class SecondRootCmp {
|
||||
}
|
||||
class SecondRootCmp {}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class TestResolver {
|
||||
resolve() {
|
||||
let resolve: (value: unknown) => void;
|
||||
const res = new Promise(r => resolve = r);
|
||||
const res = new Promise((r) => (resolve = r));
|
||||
setTimeout(() => resolve('test-data'), 0);
|
||||
return res;
|
||||
}
|
||||
|
|
@ -76,83 +91,88 @@ describe('bootstrap', () => {
|
|||
{provide: DOCUMENT, useValue: doc},
|
||||
{provide: ViewportScroller, useClass: isNode ? NullViewportScroller : ViewportScroller},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
provideNavigationEndAction(resolveFn)
|
||||
provideNavigationEndAction(resolveFn),
|
||||
];
|
||||
});
|
||||
|
||||
afterEach(destroyPlatform);
|
||||
|
||||
it('should complete when initial navigation fails and initialNavigation = enabledBlocking',
|
||||
async () => {
|
||||
@NgModule({
|
||||
imports: [BrowserModule],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [
|
||||
...testProviders,
|
||||
provideRouter(
|
||||
[{
|
||||
matcher: () => {
|
||||
throw new Error('error in matcher');
|
||||
},
|
||||
children: []
|
||||
}],
|
||||
withEnabledBlockingInitialNavigation())
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe(e => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
it('should complete when initial navigation fails and initialNavigation = enabledBlocking', async () => {
|
||||
@NgModule({
|
||||
imports: [BrowserModule],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [
|
||||
...testProviders,
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
matcher: () => {
|
||||
throw new Error('error in matcher');
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
),
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe((e) => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
|
||||
await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
|
||||
const router = res.injector.get(Router);
|
||||
expect(router.navigated).toEqual(false);
|
||||
expect(router.getCurrentNavigation()).toBeNull();
|
||||
expect(log).toContain('TestModule');
|
||||
expect(log).toContain('NavigationError');
|
||||
});
|
||||
});
|
||||
await platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((res) => {
|
||||
const router = res.injector.get(Router);
|
||||
expect(router.navigated).toEqual(false);
|
||||
expect(router.getCurrentNavigation()).toBeNull();
|
||||
expect(log).toContain('TestModule');
|
||||
expect(log).toContain('NavigationError');
|
||||
});
|
||||
});
|
||||
|
||||
it('should finish navigation when initial navigation is enabledBlocking and component renavigates on render',
|
||||
async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class Renavigate {
|
||||
constructor(router: Router) {
|
||||
router.navigateByUrl('/other');
|
||||
}
|
||||
}
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class BlankCmp {
|
||||
}
|
||||
it('should finish navigation when initial navigation is enabledBlocking and component renavigates on render', async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class Renavigate {
|
||||
constructor(router: Router) {
|
||||
router.navigateByUrl('/other');
|
||||
}
|
||||
}
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class BlankCmp {}
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterOutlet],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [
|
||||
...testProviders,
|
||||
provideRouter(
|
||||
[{path: '', component: Renavigate}, {path: 'other', component: BlankCmp}],
|
||||
withEnabledBlockingInitialNavigation())
|
||||
],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterOutlet],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [
|
||||
...testProviders,
|
||||
provideRouter(
|
||||
[
|
||||
{path: '', component: Renavigate},
|
||||
{path: 'other', component: BlankCmp},
|
||||
],
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
),
|
||||
],
|
||||
})
|
||||
class TestModule {}
|
||||
|
||||
await expectAsync(Promise.all([
|
||||
platformBrowserDynamic([]).bootstrapModule(TestModule), navigationEndPromise
|
||||
])).toBeResolved();
|
||||
});
|
||||
await expectAsync(
|
||||
Promise.all([platformBrowserDynamic([]).bootstrapModule(TestModule), navigationEndPromise]),
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('should wait for redirect when initialNavigation = enabledBlocking', async () => {
|
||||
@Injectable({providedIn: 'root'})
|
||||
|
|
@ -167,16 +187,17 @@ describe('bootstrap', () => {
|
|||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{path: 'redirectToMe', children: [], resolve: {test: TestResolver}},
|
||||
{path: '**', canActivate: [Redirect], children: []}
|
||||
],
|
||||
{initialNavigation: 'enabledBlocking'})
|
||||
[
|
||||
{path: 'redirectToMe', children: [], resolve: {test: TestResolver}},
|
||||
{path: '**', canActivate: [Redirect], children: []},
|
||||
],
|
||||
{initialNavigation: 'enabledBlocking'},
|
||||
),
|
||||
],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor() {
|
||||
|
|
@ -184,12 +205,14 @@ describe('bootstrap', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
expect(router.navigated).toEqual(true);
|
||||
expect(router.url).toContain('redirectToMe');
|
||||
expect(log).toContain('TestModule');
|
||||
});
|
||||
const bootstrapPromise = platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
expect(router.navigated).toEqual(true);
|
||||
expect(router.url).toContain('redirectToMe');
|
||||
expect(log).toContain('TestModule');
|
||||
});
|
||||
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
|
@ -199,19 +222,21 @@ describe('bootstrap', () => {
|
|||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{path: 'redirectToMe', children: []}, {
|
||||
path: '**',
|
||||
canActivate: [() => inject(Router).createUrlTree(['redirectToMe'])],
|
||||
children: []
|
||||
}
|
||||
],
|
||||
{initialNavigation: 'enabledBlocking'})
|
||||
[
|
||||
{path: 'redirectToMe', children: []},
|
||||
{
|
||||
path: '**',
|
||||
canActivate: [() => inject(Router).createUrlTree(['redirectToMe'])],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
{initialNavigation: 'enabledBlocking'},
|
||||
),
|
||||
],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor() {
|
||||
|
|
@ -219,211 +244,227 @@ describe('bootstrap', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
expect(router.navigated).toEqual(true);
|
||||
expect(router.url).toContain('redirectToMe');
|
||||
expect(log).toContain('TestModule');
|
||||
});
|
||||
const bootstrapPromise = platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
expect(router.navigated).toEqual(true);
|
||||
expect(router.url).toContain('redirectToMe');
|
||||
expect(log).toContain('TestModule');
|
||||
});
|
||||
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
||||
it('should wait for resolvers to complete when initialNavigation = enabledBlocking', async () => {
|
||||
@Component({selector: 'test', template: 'test'})
|
||||
class TestCmpEnabled {
|
||||
}
|
||||
class TestCmpEnabled {}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'enabledBlocking'})
|
||||
[{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'enabledBlocking'},
|
||||
),
|
||||
],
|
||||
declarations: [RootCmp, TestCmpEnabled],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders, TestResolver],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {}
|
||||
}
|
||||
|
||||
const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
const data = router.routerState.snapshot.root.firstChild!.data;
|
||||
expect(data['test']).toEqual('test-data');
|
||||
// Also ensure that the navigation completed. The navigation transition clears the
|
||||
// current navigation in its `finalize` operator.
|
||||
expect(router.getCurrentNavigation()).toBeNull();
|
||||
});
|
||||
const bootstrapPromise = platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((ref) => {
|
||||
const router = ref.injector.get(Router);
|
||||
const data = router.routerState.snapshot.root.firstChild!.data;
|
||||
expect(data['test']).toEqual('test-data');
|
||||
// Also ensure that the navigation completed. The navigation transition clears the
|
||||
// current navigation in its `finalize` operator.
|
||||
expect(router.getCurrentNavigation()).toBeNull();
|
||||
});
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
||||
it('should NOT wait for resolvers to complete when initialNavigation = enabledNonBlocking',
|
||||
async () => {
|
||||
@Component({selector: 'test', template: 'test'})
|
||||
class TestCmpLegacyEnabled {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'enabledNonBlocking'})
|
||||
],
|
||||
declarations: [RootCmp, TestCmpLegacyEnabled],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders, TestResolver],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe(e => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapPromise =
|
||||
platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => {
|
||||
const router: Router = ref.injector.get(Router);
|
||||
expect(router.routerState.snapshot.root.firstChild).toBeNull();
|
||||
// ResolveEnd has not been emitted yet because bootstrap returned too early
|
||||
expect(log).toEqual([
|
||||
'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
|
||||
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart'
|
||||
]);
|
||||
});
|
||||
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
||||
it('should NOT wait for resolvers to complete when initialNavigation is not set', async () => {
|
||||
it('should NOT wait for resolvers to complete when initialNavigation = enabledNonBlocking', async () => {
|
||||
@Component({selector: 'test', template: 'test'})
|
||||
class TestCmpLegacyEnabled {
|
||||
}
|
||||
class TestCmpLegacyEnabled {}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}],
|
||||
)
|
||||
[{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'enabledNonBlocking'},
|
||||
),
|
||||
],
|
||||
declarations: [RootCmp, TestCmpLegacyEnabled],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders, TestResolver],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe(e => log.push(e.constructor.name));
|
||||
router.events.subscribe((e) => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then(ref => {
|
||||
const router: Router = ref.injector.get(Router);
|
||||
expect(router.routerState.snapshot.root.firstChild).toBeNull();
|
||||
// ResolveEnd has not been emitted yet because bootstrap returned too early
|
||||
expect(log).toEqual([
|
||||
'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart',
|
||||
'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart'
|
||||
]);
|
||||
});
|
||||
const bootstrapPromise = platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((ref) => {
|
||||
const router: Router = ref.injector.get(Router);
|
||||
expect(router.routerState.snapshot.root.firstChild).toBeNull();
|
||||
// ResolveEnd has not been emitted yet because bootstrap returned too early
|
||||
expect(log).toEqual([
|
||||
'TestModule',
|
||||
'RootCmp',
|
||||
'NavigationStart',
|
||||
'RoutesRecognized',
|
||||
'GuardsCheckStart',
|
||||
'ChildActivationStart',
|
||||
'ActivationStart',
|
||||
'GuardsCheckEnd',
|
||||
'ResolveStart',
|
||||
]);
|
||||
});
|
||||
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
||||
it('should NOT wait for resolvers to complete when initialNavigation is not set', async () => {
|
||||
@Component({selector: 'test', template: 'test'})
|
||||
class TestCmpLegacyEnabled {}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot([
|
||||
{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}},
|
||||
]),
|
||||
],
|
||||
declarations: [RootCmp, TestCmpLegacyEnabled],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders, TestResolver],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe((e) => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapPromise = platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((ref) => {
|
||||
const router: Router = ref.injector.get(Router);
|
||||
expect(router.routerState.snapshot.root.firstChild).toBeNull();
|
||||
// ResolveEnd has not been emitted yet because bootstrap returned too early
|
||||
expect(log).toEqual([
|
||||
'TestModule',
|
||||
'RootCmp',
|
||||
'NavigationStart',
|
||||
'RoutesRecognized',
|
||||
'GuardsCheckStart',
|
||||
'ChildActivationStart',
|
||||
'ActivationStart',
|
||||
'GuardsCheckEnd',
|
||||
'ResolveStart',
|
||||
]);
|
||||
});
|
||||
|
||||
await Promise.all([bootstrapPromise, navigationEndPromise]);
|
||||
});
|
||||
|
||||
it('should not run navigation when initialNavigation = disabled', (done) => {
|
||||
@Component({selector: 'test', template: 'test'})
|
||||
class TestCmpDiabled {
|
||||
}
|
||||
class TestCmpDiabled {}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'disabled'})
|
||||
[{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}],
|
||||
{initialNavigation: 'disabled'},
|
||||
),
|
||||
],
|
||||
declarations: [RootCmp, TestCmpDiabled],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders, TestResolver],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
constructor(router: Router) {
|
||||
log.push('TestModule');
|
||||
router.events.subscribe(e => log.push(e.constructor.name));
|
||||
router.events.subscribe((e) => log.push(e.constructor.name));
|
||||
}
|
||||
}
|
||||
|
||||
platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
|
||||
const router = res.injector.get(Router);
|
||||
expect(log).toEqual(['TestModule', 'RootCmp']);
|
||||
done();
|
||||
});
|
||||
platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((res) => {
|
||||
const router = res.injector.get(Router);
|
||||
expect(log).toEqual(['TestModule', 'RootCmp']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not init router navigation listeners if a non root component is bootstrapped',
|
||||
async () => {
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[],
|
||||
)
|
||||
],
|
||||
declarations: [SecondRootCmp, RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: testProviders,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
it('should not init router navigation listeners if a non root component is bootstrapped', async () => {
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterModule.forRoot([])],
|
||||
declarations: [SecondRootCmp, RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: testProviders,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {}
|
||||
|
||||
await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
|
||||
const router = res.injector.get(Router);
|
||||
spyOn(router as any, 'resetRootComponentType').and.callThrough();
|
||||
await platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((res) => {
|
||||
const router = res.injector.get(Router);
|
||||
spyOn(router as any, 'resetRootComponentType').and.callThrough();
|
||||
|
||||
const appRef: ApplicationRef = res.injector.get(ApplicationRef);
|
||||
appRef.bootstrap(SecondRootCmp);
|
||||
const appRef: ApplicationRef = res.injector.get(ApplicationRef);
|
||||
appRef.bootstrap(SecondRootCmp);
|
||||
|
||||
expect((router as any).resetRootComponentType).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect((router as any).resetRootComponentType).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed',
|
||||
async () => {
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterModule.forRoot([])],
|
||||
declarations: [SecondRootCmp, RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: testProviders,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', async () => {
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterModule.forRoot([])],
|
||||
declarations: [SecondRootCmp, RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: testProviders,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {}
|
||||
|
||||
await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => {
|
||||
const router = res.injector.get(Router);
|
||||
spyOn(router as any, 'resetRootComponentType').and.callThrough();
|
||||
await platformBrowserDynamic([])
|
||||
.bootstrapModule(TestModule)
|
||||
.then((res) => {
|
||||
const router = res.injector.get(Router);
|
||||
spyOn(router as any, 'resetRootComponentType').and.callThrough();
|
||||
|
||||
const appRef: ApplicationRef = res.injector.get(ApplicationRef);
|
||||
const {promise, resolveFn} = createPromise();
|
||||
appRef.components[0].onDestroy(() => {
|
||||
appRef.bootstrap(SecondRootCmp);
|
||||
expect((router as any).resetRootComponentType).toHaveBeenCalled();
|
||||
resolveFn();
|
||||
});
|
||||
const appRef: ApplicationRef = res.injector.get(ApplicationRef);
|
||||
const {promise, resolveFn} = createPromise();
|
||||
appRef.components[0].onDestroy(() => {
|
||||
appRef.bootstrap(SecondRootCmp);
|
||||
expect((router as any).resetRootComponentType).toHaveBeenCalled();
|
||||
resolveFn();
|
||||
});
|
||||
|
||||
appRef.components[0].destroy();
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
appRef.components[0].destroy();
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
|
||||
if (!isNode) {
|
||||
it('should restore the scrolling position', async () => {
|
||||
|
|
@ -437,34 +478,34 @@ describe('bootstrap', () => {
|
|||
<div style="height: 3000px;"></div>
|
||||
<a name="marker3"></a>
|
||||
<div style="height: 3000px;"></div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
class TallComponent {
|
||||
}
|
||||
class TallComponent {}
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{path: '', pathMatch: 'full', redirectTo: '/aa'},
|
||||
{path: 'aa', component: TallComponent}, {path: 'bb', component: TallComponent},
|
||||
{path: 'cc', component: TallComponent},
|
||||
{path: 'fail', component: TallComponent, canActivate: [() => false]}
|
||||
],
|
||||
{
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
scrollOffset: [0, 100],
|
||||
onSameUrlNavigation: 'ignore',
|
||||
})
|
||||
[
|
||||
{path: '', pathMatch: 'full', redirectTo: '/aa'},
|
||||
{path: 'aa', component: TallComponent},
|
||||
{path: 'bb', component: TallComponent},
|
||||
{path: 'cc', component: TallComponent},
|
||||
{path: 'fail', component: TallComponent, canActivate: [() => false]},
|
||||
],
|
||||
{
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
scrollOffset: [0, 100],
|
||||
onSameUrlNavigation: 'ignore',
|
||||
},
|
||||
),
|
||||
],
|
||||
declarations: [TallComponent, RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
class TestModule {}
|
||||
|
||||
function resolveAfter(milliseconds: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
|
|
@ -495,7 +536,7 @@ describe('bootstrap', () => {
|
|||
await router.navigateByUrl('/aa#marker2');
|
||||
await resolveAfter(100);
|
||||
expect(window.scrollY).toBeGreaterThanOrEqual(5900);
|
||||
expect(window.scrollY).toBeLessThan(6000); // offset
|
||||
expect(window.scrollY).toBeLessThan(6000); // offset
|
||||
|
||||
// Scroll somewhere else, then navigate to the hash again. Even though the same url navigation
|
||||
// is ignored by the Router, we should still scroll.
|
||||
|
|
@ -503,7 +544,7 @@ describe('bootstrap', () => {
|
|||
await router.navigateByUrl('/aa#marker2');
|
||||
await resolveAfter(100);
|
||||
expect(window.scrollY).toBeGreaterThanOrEqual(5900);
|
||||
expect(window.scrollY).toBeLessThan(6000); // offset
|
||||
expect(window.scrollY).toBeLessThan(6000); // offset
|
||||
});
|
||||
|
||||
it('should cleanup "popstate" and "hashchange" listeners', async () => {
|
||||
|
|
@ -511,11 +552,12 @@ describe('bootstrap', () => {
|
|||
imports: [BrowserModule, RouterModule.forRoot([])],
|
||||
declarations: [RootCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers:
|
||||
[...testProviders, {provide: PlatformLocation, useClass: BrowserPlatformLocation}],
|
||||
providers: [
|
||||
...testProviders,
|
||||
{provide: PlatformLocation, useClass: BrowserPlatformLocation},
|
||||
],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
class TestModule {}
|
||||
|
||||
spyOn(window, 'addEventListener').and.callThrough();
|
||||
spyOn(window, 'removeEventListener').and.callThrough();
|
||||
|
|
@ -525,10 +567,16 @@ describe('bootstrap', () => {
|
|||
|
||||
expect(window.addEventListener).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(window.addEventListener)
|
||||
.toHaveBeenCalledWith('popstate', jasmine.any(Function), jasmine.any(Boolean));
|
||||
expect(window.addEventListener)
|
||||
.toHaveBeenCalledWith('hashchange', jasmine.any(Function), jasmine.any(Boolean));
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'popstate',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(Boolean),
|
||||
);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'hashchange',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(Boolean),
|
||||
);
|
||||
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith('popstate', jasmine.any(Function));
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function));
|
||||
|
|
@ -539,19 +587,16 @@ describe('bootstrap', () => {
|
|||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{path: 'a', component: SimpleCmp},
|
||||
{path: 'b', component: SimpleCmp},
|
||||
],
|
||||
)
|
||||
RouterModule.forRoot([
|
||||
{path: 'a', component: SimpleCmp},
|
||||
{path: 'b', component: SimpleCmp},
|
||||
]),
|
||||
],
|
||||
declarations: [RootCmp, SimpleCmp],
|
||||
bootstrap: [RootCmp],
|
||||
providers: [...testProviders],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
class TestModule {}
|
||||
|
||||
(async () => {
|
||||
const res = await platformBrowserDynamic([]).bootstrapModule(TestModule);
|
||||
|
|
@ -568,7 +613,7 @@ describe('bootstrap', () => {
|
|||
});
|
||||
|
||||
function onNavigationEnd(router: Router, fn: Function) {
|
||||
router.events.subscribe(e => {
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationEnd) {
|
||||
fn();
|
||||
}
|
||||
|
|
@ -581,13 +626,13 @@ function provideNavigationEndAction(fn: Function) {
|
|||
multi: true,
|
||||
useValue: () => {
|
||||
onNavigationEnd(inject(Router), fn);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPromise() {
|
||||
let resolveFn: () => void;
|
||||
const promise = new Promise<void>(r => {
|
||||
const promise = new Promise<void>((r) => {
|
||||
resolveFn = r;
|
||||
});
|
||||
return {resolveFn: () => resolveFn(), promise};
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ describe('`restoredState#ɵrouterPageId`', () => {
|
|||
@Injectable({providedIn: 'root'})
|
||||
class MyCanActivateGuard {
|
||||
allow: boolean = true;
|
||||
redirectTo: string|null|UrlTree = null;
|
||||
redirectTo: string | null | UrlTree = null;
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
canActivate(): boolean|UrlTree {
|
||||
canActivate(): boolean | UrlTree {
|
||||
if (typeof this.redirectTo === 'string') {
|
||||
this.router.navigateByUrl(this.redirectTo);
|
||||
} else if (isUrlTree(this.redirectTo)) {
|
||||
|
|
@ -66,55 +66,56 @@ describe('`restoredState#ɵrouterPageId`', () => {
|
|||
|
||||
let fixture: ComponentFixture<unknown>;
|
||||
|
||||
function createNavigationHistory(urlUpdateStrategy: 'eager'|'deferred' = 'deferred') {
|
||||
function createNavigationHistory(urlUpdateStrategy: 'eager' | 'deferred' = 'deferred') {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TestModule],
|
||||
providers: [
|
||||
{provide: 'alwaysFalse', useValue: (a: any) => false},
|
||||
{provide: Location, useClass: SpyLocation},
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'first',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve}
|
||||
},
|
||||
{
|
||||
path: 'second',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve}
|
||||
},
|
||||
{
|
||||
path: 'third',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve}
|
||||
},
|
||||
{
|
||||
path: 'unguarded',
|
||||
component: SimpleCmp,
|
||||
},
|
||||
{
|
||||
path: 'throwing',
|
||||
component: ThrowingCmp,
|
||||
},
|
||||
{
|
||||
path: 'loaded',
|
||||
loadChildren: () => of(ModuleWithSimpleCmpAsRoute),
|
||||
canLoad: ['alwaysFalse']
|
||||
}
|
||||
],
|
||||
withRouterConfig({
|
||||
urlUpdateStrategy,
|
||||
canceledNavigationResolution: 'computed',
|
||||
resolveNavigationPromiseOnError: true,
|
||||
})),
|
||||
]
|
||||
[
|
||||
{
|
||||
path: 'first',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve},
|
||||
},
|
||||
{
|
||||
path: 'second',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve},
|
||||
},
|
||||
{
|
||||
path: 'third',
|
||||
component: SimpleCmp,
|
||||
canDeactivate: [MyCanDeactivateGuard],
|
||||
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
||||
resolve: {x: MyResolve},
|
||||
},
|
||||
{
|
||||
path: 'unguarded',
|
||||
component: SimpleCmp,
|
||||
},
|
||||
{
|
||||
path: 'throwing',
|
||||
component: ThrowingCmp,
|
||||
},
|
||||
{
|
||||
path: 'loaded',
|
||||
loadChildren: () => of(ModuleWithSimpleCmpAsRoute),
|
||||
canLoad: ['alwaysFalse'],
|
||||
},
|
||||
],
|
||||
withRouterConfig({
|
||||
urlUpdateStrategy,
|
||||
canceledNavigationResolution: 'computed',
|
||||
resolveNavigationPromiseOnError: true,
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
|
|
@ -146,222 +147,216 @@ describe('`restoredState#ɵrouterPageId`', () => {
|
|||
}));
|
||||
|
||||
it('should work when CanActivate returns false', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).allow = true;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
TestBed.inject(MyCanActivateGuard).allow = true;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
|
||||
router.navigateByUrl('/second');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
|
||||
router.navigateByUrl('/second');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
|
||||
it('should work when CanDeactivate returns false', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
TestBed.inject(MyCanDeactivateGuard).allow = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
TestBed.inject(MyCanDeactivateGuard).allow = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
|
||||
TestBed.inject(MyCanDeactivateGuard).allow = true;
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/third');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
}));
|
||||
TestBed.inject(MyCanDeactivateGuard).allow = true;
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/third');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
}));
|
||||
|
||||
it('should work when using `NavigationExtras.skipLocationChange`', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('/first', {skipLocationChange: true});
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/first', {skipLocationChange: true});
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
|
||||
it('should work when using `NavigationExtras.replaceUrl`', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('/first', {replaceUrl: true});
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
}));
|
||||
router.navigateByUrl('/first', {replaceUrl: true});
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
}));
|
||||
|
||||
it('should work when CanLoad returns false', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
router.navigateByUrl('/loaded');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
router.navigateByUrl('/loaded');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
|
||||
it('should work when resolve empty', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = of(2);
|
||||
TestBed.inject(MyResolve).myresolve = of(2);
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
|
||||
// We should cancel the navigation to `/third` when myresolve is empty
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
// We should cancel the navigation to `/third` when myresolve is empty
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
|
||||
location.historyGo(2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
location.historyGo(2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
expect(location.path()).toEqual('/first');
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = of(2);
|
||||
location.historyGo(2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
expect(location.path()).toEqual('/third');
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
location.historyGo(-2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
expect(location.path()).toEqual('/third');
|
||||
}));
|
||||
TestBed.inject(MyResolve).myresolve = of(2);
|
||||
location.historyGo(2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
expect(location.path()).toEqual('/third');
|
||||
|
||||
TestBed.inject(MyResolve).myresolve = EMPTY;
|
||||
location.historyGo(-2);
|
||||
advance(fixture);
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
expect(location.path()).toEqual('/third');
|
||||
}));
|
||||
|
||||
it('should work when an error occurred during navigation', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('/invalid').catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
router.navigateByUrl('/invalid').catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
|
||||
it('should work when CanActivate redirects', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const location = TestBed.inject(Location);
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
|
||||
it('restores history correctly when component throws error in constructor and replaceUrl=true',
|
||||
fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
it('restores history correctly when component throws error in constructor and replaceUrl=true', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
router.navigateByUrl('/throwing', {replaceUrl: true}).catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/throwing', {replaceUrl: true}).catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
|
||||
it('restores history correctly when component throws error in constructor and skipLocationChange=true',
|
||||
fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
it('restores history correctly when component throws error in constructor and skipLocationChange=true', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
router.navigateByUrl('/throwing', {skipLocationChange: true}).catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/throwing', {skipLocationChange: true}).catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('eager url updates', () => {
|
||||
|
|
@ -369,147 +364,142 @@ describe('`restoredState#ɵrouterPageId`', () => {
|
|||
createNavigationHistory('eager');
|
||||
}));
|
||||
it('should work', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location) as SpyLocation;
|
||||
const router = TestBed.inject(Router);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
const location = TestBed.inject(Location) as SpyLocation;
|
||||
const router = TestBed.inject(Router);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
router.navigateByUrl('/first');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
TestBed.inject(MyCanActivateGuard).allow = false;
|
||||
router.navigateByUrl('/first');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
it('should work when CanActivate redirects', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/third');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/third');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
}));
|
||||
it('should work when CanActivate redirects with UrlTree', fakeAsync(() => {
|
||||
// Note that this test is different from the above case because we are able to specifically
|
||||
// handle the `UrlTree` case as a proper redirect and set `replaceUrl: true` on the
|
||||
// follow-up navigation.
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
let allowNavigation = true;
|
||||
router.resetConfig([
|
||||
{path: 'initial', children: []},
|
||||
{path: 'redirectFrom', redirectTo: 'redirectTo'},
|
||||
{path: 'redirectTo', children: [], canActivate: [() => allowNavigation]},
|
||||
]);
|
||||
// Note that this test is different from the above case because we are able to specifically
|
||||
// handle the `UrlTree` case as a proper redirect and set `replaceUrl: true` on the
|
||||
// follow-up navigation.
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
let allowNavigation = true;
|
||||
router.resetConfig([
|
||||
{path: 'initial', children: []},
|
||||
{path: 'redirectFrom', redirectTo: 'redirectTo'},
|
||||
{path: 'redirectTo', children: [], canActivate: [() => allowNavigation]},
|
||||
]);
|
||||
|
||||
// already at '2' from the `beforeEach` navigations
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/initial');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/initial');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
// already at '2' from the `beforeEach` navigations
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/initial');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/initial');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
|
||||
router.navigateByUrl('redirectTo');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
router.navigateByUrl('redirectTo');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
|
||||
// Navigate to different URL but get redirected to same URL should result in same page id
|
||||
router.navigateByUrl('redirectFrom');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
// Navigate to different URL but get redirected to same URL should result in same page id
|
||||
router.navigateByUrl('redirectFrom');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
|
||||
// Back and forward should have page IDs 1 apart
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/initial');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
// Back and forward should have page IDs 1 apart
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/initial');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
location.forward();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
|
||||
// Rejected navigation after redirect to same URL should have the same page ID
|
||||
allowNavigation = false;
|
||||
router.navigateByUrl('redirectFrom');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
}));
|
||||
// Rejected navigation after redirect to same URL should have the same page ID
|
||||
allowNavigation = false;
|
||||
router.navigateByUrl('redirectFrom');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/redirectTo');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
||||
}));
|
||||
it('redirectTo with same url, and guard reject', fakeAsync(() => {
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = router.createUrlTree(['unguarded']);
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = router.createUrlTree(['unguarded']);
|
||||
router.navigateByUrl('/third');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/unguarded');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
||||
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
for (const urlUpdateStrategy of ['deferred', 'eager'] as const) {
|
||||
it(`restores history correctly when an error is thrown in guard with urlUpdateStrategy ${
|
||||
urlUpdateStrategy}`,
|
||||
fakeAsync(() => {
|
||||
createNavigationHistory(urlUpdateStrategy);
|
||||
const location = TestBed.inject(Location);
|
||||
it(`restores history correctly when an error is thrown in guard with urlUpdateStrategy ${urlUpdateStrategy}`, fakeAsync(() => {
|
||||
createNavigationHistory(urlUpdateStrategy);
|
||||
const location = TestBed.inject(Location);
|
||||
|
||||
TestBed.inject(ThrowingCanActivateGuard).throw = true;
|
||||
TestBed.inject(ThrowingCanActivateGuard).throw = true;
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
TestBed.inject(ThrowingCanActivateGuard).throw = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
TestBed.inject(ThrowingCanActivateGuard).throw = false;
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
|
||||
it(`restores history correctly when component throws error in constructor with urlUpdateStrategy ${
|
||||
urlUpdateStrategy}`,
|
||||
fakeAsync(() => {
|
||||
createNavigationHistory(urlUpdateStrategy);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
it(`restores history correctly when component throws error in constructor with urlUpdateStrategy ${urlUpdateStrategy}`, fakeAsync(() => {
|
||||
createNavigationHistory(urlUpdateStrategy);
|
||||
const location = TestBed.inject(Location);
|
||||
const router = TestBed.inject(Router);
|
||||
|
||||
router.navigateByUrl('/throwing').catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
router.navigateByUrl('/throwing').catch(() => null);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/second');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
||||
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
location.back();
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/first');
|
||||
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -522,18 +512,13 @@ function createRoot<T>(router: Router, type: Type<T>): ComponentFixture<T> {
|
|||
}
|
||||
|
||||
@Component({selector: 'simple-cmp', template: `simple`})
|
||||
class SimpleCmp {
|
||||
}
|
||||
class SimpleCmp {}
|
||||
|
||||
@NgModule(
|
||||
{imports: [RouterModule.forChild([{path: '', component: SimpleCmp}])]},
|
||||
)
|
||||
class ModuleWithSimpleCmpAsRoute {
|
||||
}
|
||||
@NgModule({imports: [RouterModule.forChild([{path: '', component: SimpleCmp}])]})
|
||||
class ModuleWithSimpleCmpAsRoute {}
|
||||
|
||||
@Component({selector: 'root-cmp', template: `<router-outlet></router-outlet>`})
|
||||
class RootCmp {
|
||||
}
|
||||
class RootCmp {}
|
||||
|
||||
@Component({selector: 'throwing-cmp', template: ''})
|
||||
class ThrowingCmp {
|
||||
|
|
@ -542,23 +527,15 @@ class ThrowingCmp {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function advance(fixture: ComponentFixture<unknown>, millis?: number): void {
|
||||
tick(millis);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
CommonModule,
|
||||
],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
],
|
||||
imports: [RouterOutlet, CommonModule],
|
||||
providers: [provideLocationMocks()],
|
||||
exports: [SimpleCmp, RootCmp, ThrowingCmp],
|
||||
declarations: [SimpleCmp, RootCmp, ThrowingCmp]
|
||||
declarations: [SimpleCmp, RootCmp, ThrowingCmp],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
class TestModule {}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ import {validateConfig} from '../src/utils/config';
|
|||
describe('config', () => {
|
||||
describe('validateConfig', () => {
|
||||
it('should not throw when no errors', () => {
|
||||
expect(
|
||||
() => validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}]))
|
||||
.not.toThrow();
|
||||
expect(() =>
|
||||
validateConfig([
|
||||
{path: 'a', redirectTo: 'b'},
|
||||
{path: 'b', component: ComponentA},
|
||||
]),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when a matcher is provided', () => {
|
||||
|
|
@ -24,20 +27,22 @@ describe('config', () => {
|
|||
|
||||
it('should throw for undefined route', () => {
|
||||
expect(() => {
|
||||
validateConfig(
|
||||
[{path: 'a', component: ComponentA}, , {path: 'b', component: ComponentB}] as Routes);
|
||||
validateConfig([
|
||||
{path: 'a', component: ComponentA},
|
||||
,
|
||||
{path: 'b', component: ComponentB},
|
||||
] as Routes);
|
||||
}).toThrowError(/Invalid configuration of route ''/);
|
||||
});
|
||||
|
||||
it('should throw for undefined route in children', () => {
|
||||
expect(() => {
|
||||
validateConfig([{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
,
|
||||
]
|
||||
}] as Routes);
|
||||
validateConfig([
|
||||
{
|
||||
path: 'a',
|
||||
children: [{path: 'b', component: ComponentB}, ,],
|
||||
},
|
||||
] as Routes);
|
||||
}).toThrowError(/Invalid configuration of route 'a'/);
|
||||
});
|
||||
|
||||
|
|
@ -45,15 +50,19 @@ describe('config', () => {
|
|||
expect(() => {
|
||||
validateConfig([
|
||||
{path: 'a', component: ComponentA},
|
||||
[{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}]
|
||||
[
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC},
|
||||
],
|
||||
] as Routes);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw when redirectTo and children are used together', () => {
|
||||
expect(() => {
|
||||
validateConfig(
|
||||
[{path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]}]);
|
||||
validateConfig([
|
||||
{path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]},
|
||||
]);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
|
|
@ -62,11 +71,11 @@ describe('config', () => {
|
|||
});
|
||||
|
||||
it('should properly report deeply nested path', () => {
|
||||
expect(
|
||||
() => validateConfig([
|
||||
{path: 'a', children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]}
|
||||
]))
|
||||
.toThrowError();
|
||||
expect(() =>
|
||||
validateConfig([
|
||||
{path: 'a', children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]},
|
||||
]),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw when redirectTo and loadChildren are used together', () => {
|
||||
|
|
@ -84,9 +93,11 @@ describe('config', () => {
|
|||
it('should throw when component and redirectTo are used together', () => {
|
||||
expect(() => {
|
||||
validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]);
|
||||
})
|
||||
.toThrowError(new RegExp(
|
||||
`Invalid configuration of route 'a': redirectTo and component/loadComponent cannot be used together`));
|
||||
}).toThrowError(
|
||||
new RegExp(
|
||||
`Invalid configuration of route 'a': redirectTo and component/loadComponent cannot be used together`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when redirectTo and loadComponent are used together', () => {
|
||||
|
|
@ -131,22 +142,22 @@ describe('config', () => {
|
|||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw when emptyPath is used with redirectTo without explicitly providing matching',
|
||||
() => {
|
||||
expect(() => {
|
||||
validateConfig([{path: '', redirectTo: 'b'}]);
|
||||
}).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/);
|
||||
});
|
||||
it('should throw when emptyPath is used with redirectTo without explicitly providing matching', () => {
|
||||
expect(() => {
|
||||
validateConfig([{path: '', redirectTo: 'b'}]);
|
||||
}).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/);
|
||||
});
|
||||
|
||||
it('should throw when path/outlet combination is invalid', () => {
|
||||
expect(() => {
|
||||
validateConfig([{path: 'a', outlet: 'aux'}]);
|
||||
})
|
||||
.toThrowError(
|
||||
/Invalid configuration of route 'a': a componentless route without children or loadChildren cannot have a named outlet set/);
|
||||
}).toThrowError(
|
||||
/Invalid configuration of route 'a': a componentless route without children or loadChildren cannot have a named outlet set/,
|
||||
);
|
||||
expect(() => validateConfig([{path: 'a', outlet: '', children: []}])).not.toThrow();
|
||||
expect(() => validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]))
|
||||
.not.toThrow();
|
||||
expect(() =>
|
||||
validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when path/outlet combination is valid', () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,13 @@ import {Routes} from '../src/models';
|
|||
import {recognize} from '../src/recognize';
|
||||
import {DefaultRouteReuseStrategy} from '../src/route_reuse_strategy';
|
||||
import {RouterConfigLoader} from '../src/router_config_loader';
|
||||
import {ActivatedRoute, advanceActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from '../src/router_state';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
advanceActivatedRoute,
|
||||
createEmptyState,
|
||||
RouterState,
|
||||
RouterStateSnapshot,
|
||||
} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
|
|
@ -30,15 +36,17 @@ describe('create router state', async () => {
|
|||
|
||||
it('should create new state', async () => {
|
||||
const state = createRouterState(
|
||||
reuseStrategy,
|
||||
await createState(
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)'),
|
||||
emptyState());
|
||||
reuseStrategy,
|
||||
await createState(
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'},
|
||||
],
|
||||
'a(left:b//right:c)',
|
||||
),
|
||||
emptyState(),
|
||||
);
|
||||
|
||||
checkActivatedRoute(state.root, RootComponent);
|
||||
|
||||
|
|
@ -50,15 +58,22 @@ describe('create router state', async () => {
|
|||
|
||||
it('should reuse existing nodes when it can', async () => {
|
||||
const config = [
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'},
|
||||
];
|
||||
|
||||
const prevState =
|
||||
createRouterState(reuseStrategy, await createState(config, 'a(left:b)'), emptyState());
|
||||
const prevState = createRouterState(
|
||||
reuseStrategy,
|
||||
await createState(config, 'a(left:b)'),
|
||||
emptyState(),
|
||||
);
|
||||
advanceState(prevState);
|
||||
const state =
|
||||
createRouterState(reuseStrategy, await createState(config, 'a(left:c)'), prevState);
|
||||
const state = createRouterState(
|
||||
reuseStrategy,
|
||||
await createState(config, 'a(left:c)'),
|
||||
prevState,
|
||||
);
|
||||
|
||||
expect(prevState.root).toBe(state.root);
|
||||
const prevC = (prevState as any).children(prevState.root);
|
||||
|
|
@ -70,18 +85,27 @@ describe('create router state', async () => {
|
|||
});
|
||||
|
||||
it('should handle componentless routes', async () => {
|
||||
const config = [{
|
||||
path: 'a/:id',
|
||||
children:
|
||||
[{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}]
|
||||
}];
|
||||
|
||||
const config = [
|
||||
{
|
||||
path: 'a/:id',
|
||||
children: [
|
||||
{path: 'b', component: ComponentA},
|
||||
{path: 'c', component: ComponentB, outlet: 'right'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const prevState = createRouterState(
|
||||
reuseStrategy, await createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
|
||||
reuseStrategy,
|
||||
await createState(config, 'a/1;p=11/(b//right:c)'),
|
||||
emptyState(),
|
||||
);
|
||||
advanceState(prevState);
|
||||
const state = createRouterState(
|
||||
reuseStrategy, await createState(config, 'a/2;p=22/(b//right:c)'), prevState);
|
||||
reuseStrategy,
|
||||
await createState(config, 'a/2;p=22/(b//right:c)'),
|
||||
prevState,
|
||||
);
|
||||
|
||||
expect(prevState.root).toBe(state.root);
|
||||
const prevP = (prevState as any).firstChild(prevState.root)!;
|
||||
|
|
@ -99,13 +123,17 @@ describe('create router state', async () => {
|
|||
|
||||
it('should not retrieve routes when `shouldAttach` is always false', async () => {
|
||||
const config: Routes = [
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'},
|
||||
];
|
||||
spyOn(reuseStrategy, 'retrieve');
|
||||
|
||||
const prevState =
|
||||
createRouterState(reuseStrategy, await createState(config, 'a(left:b)'), emptyState());
|
||||
const prevState = createRouterState(
|
||||
reuseStrategy,
|
||||
await createState(config, 'a(left:b)'),
|
||||
emptyState(),
|
||||
);
|
||||
advanceState(prevState);
|
||||
createRouterState(reuseStrategy, await createState(config, 'a(left:c)'), prevState);
|
||||
expect(reuseStrategy.retrieve).not.toHaveBeenCalled();
|
||||
|
|
@ -114,11 +142,14 @@ describe('create router state', async () => {
|
|||
it('should consistently represent future and current state', async () => {
|
||||
const config: Routes = [
|
||||
{path: '', pathMatch: 'full', component: ComponentA},
|
||||
{path: 'product/:id', component: ComponentB}
|
||||
{path: 'product/:id', component: ComponentB},
|
||||
];
|
||||
spyOn(reuseStrategy, 'shouldReuseRoute').and.callThrough();
|
||||
const previousState =
|
||||
createRouterState(reuseStrategy, await createState(config, ''), emptyState());
|
||||
const previousState = createRouterState(
|
||||
reuseStrategy,
|
||||
await createState(config, ''),
|
||||
emptyState(),
|
||||
);
|
||||
advanceState(previousState);
|
||||
(reuseStrategy.shouldReuseRoute as jasmine.Spy).calls.reset();
|
||||
|
||||
|
|
@ -151,14 +182,22 @@ function advanceNode(node: TreeNode<ActivatedRoute>): void {
|
|||
|
||||
async function createState(config: Routes, url: string): Promise<RouterStateSnapshot> {
|
||||
return recognize(
|
||||
TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader), RootComponent,
|
||||
config, tree(url), new DefaultUrlSerializer())
|
||||
.pipe(map(result => result.state))
|
||||
.toPromise() as Promise<RouterStateSnapshot>;
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
TestBed.inject(RouterConfigLoader),
|
||||
RootComponent,
|
||||
config,
|
||||
tree(url),
|
||||
new DefaultUrlSerializer(),
|
||||
)
|
||||
.pipe(map((result) => result.state))
|
||||
.toPromise() as Promise<RouterStateSnapshot>;
|
||||
}
|
||||
|
||||
function checkActivatedRoute(
|
||||
actual: ActivatedRoute, cmp: Function, outlet: string = PRIMARY_OUTLET): void {
|
||||
actual: ActivatedRoute,
|
||||
cmp: Function,
|
||||
outlet: string = PRIMARY_OUTLET,
|
||||
): void {
|
||||
if (actual === null) {
|
||||
expect(actual).toBeDefined();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ describe('createUrlTree', async () => {
|
|||
children: [
|
||||
{path: 'child', component: class {}},
|
||||
{path: '**', outlet: 'secondary', component: class {}},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'a',
|
||||
|
|
@ -37,7 +37,7 @@ describe('createUrlTree', async () => {
|
|||
{path: '**', component: class {}},
|
||||
{path: '**', outlet: 'right', component: class {}},
|
||||
{path: '**', outlet: 'left', component: class {}},
|
||||
]
|
||||
],
|
||||
},
|
||||
{path: '**', component: class {}},
|
||||
{path: '**', outlet: 'right', component: class {}},
|
||||
|
|
@ -89,9 +89,9 @@ describe('createUrlTree', async () => {
|
|||
|
||||
it('should error when navigating to the root segment with params', async () => {
|
||||
const p = serializer.parse('/');
|
||||
await expectAsync(createRoot(p, [
|
||||
'/', {p: 11}
|
||||
])).toBeRejectedWithError(/Root segment cannot have matrix parameters/);
|
||||
await expectAsync(createRoot(p, ['/', {p: 11}])).toBeRejectedWithError(
|
||||
/Root segment cannot have matrix parameters/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should support nested segments', async () => {
|
||||
|
|
@ -155,14 +155,13 @@ describe('createUrlTree', async () => {
|
|||
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
|
||||
});
|
||||
|
||||
it('should support updating two outlets at the same time relative to non-root segment',
|
||||
async () => {
|
||||
await router.navigateByUrl('/parent/child');
|
||||
const t = create(
|
||||
router.routerState.root.children[0],
|
||||
[{outlets: {primary: 'child', secondary: 'popup'}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
|
||||
});
|
||||
it('should support updating two outlets at the same time relative to non-root segment', async () => {
|
||||
await router.navigateByUrl('/parent/child');
|
||||
const t = create(router.routerState.root.children[0], [
|
||||
{outlets: {primary: 'child', secondary: 'popup'}},
|
||||
]);
|
||||
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
|
||||
});
|
||||
|
||||
it('should support adding multiple outlets with prefix', async () => {
|
||||
const p = serializer.parse('');
|
||||
|
|
@ -205,63 +204,68 @@ describe('createUrlTree', async () => {
|
|||
expect(serializer.serialize(t)).toEqual('/parent/child(rootSecondary:rootPopup)');
|
||||
});
|
||||
|
||||
it('works with named children of empty path primary, relative to non-empty parent',
|
||||
async () => {
|
||||
router.resetConfig([{
|
||||
path: 'case',
|
||||
component: class {},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: class {},
|
||||
children: [
|
||||
{path: 'foo', outlet: 'foo', children: []},
|
||||
],
|
||||
},
|
||||
]
|
||||
}]);
|
||||
await router.navigateByUrl('/case');
|
||||
expect(router.url).toEqual('/case');
|
||||
expect(router
|
||||
.createUrlTree(
|
||||
[{outlets: {'foo': ['foo']}}],
|
||||
// relative to the 'case' route
|
||||
{relativeTo: router.routerState.root.firstChild})
|
||||
.toString())
|
||||
.toEqual('/case/(foo:foo)');
|
||||
});
|
||||
it('works with named children of empty path primary, relative to non-empty parent', async () => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'case',
|
||||
component: class {},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: class {},
|
||||
children: [{path: 'foo', outlet: 'foo', children: []}],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
await router.navigateByUrl('/case');
|
||||
expect(router.url).toEqual('/case');
|
||||
expect(
|
||||
router
|
||||
.createUrlTree(
|
||||
[{outlets: {'foo': ['foo']}}],
|
||||
// relative to the 'case' route
|
||||
{relativeTo: router.routerState.root.firstChild},
|
||||
)
|
||||
.toString(),
|
||||
).toEqual('/case/(foo:foo)');
|
||||
});
|
||||
|
||||
it('can change both primary and named outlets under an empty path', async () => {
|
||||
router.resetConfig([{
|
||||
path: 'foo',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: class {},
|
||||
children: [
|
||||
{path: 'bar', component: class {}},
|
||||
{path: 'baz', component: class {}, outlet: 'other'},
|
||||
],
|
||||
},
|
||||
]
|
||||
}]);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'foo',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: class {},
|
||||
children: [
|
||||
{path: 'bar', component: class {}},
|
||||
{path: 'baz', component: class {}, outlet: 'other'},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await router.navigateByUrl('/foo/(bar//other:baz)');
|
||||
expect(router.url).toEqual('/foo/(bar//other:baz)');
|
||||
expect(router
|
||||
.createUrlTree(
|
||||
[
|
||||
{
|
||||
outlets: {
|
||||
other: null,
|
||||
primary: ['bar'],
|
||||
},
|
||||
},
|
||||
],
|
||||
// relative to the root '' route
|
||||
{relativeTo: router.routerState.root.firstChild})
|
||||
.toString())
|
||||
.toEqual('/foo/bar');
|
||||
expect(
|
||||
router
|
||||
.createUrlTree(
|
||||
[
|
||||
{
|
||||
outlets: {
|
||||
other: null,
|
||||
primary: ['bar'],
|
||||
},
|
||||
},
|
||||
],
|
||||
// relative to the root '' route
|
||||
{relativeTo: router.routerState.root.firstChild},
|
||||
)
|
||||
.toString(),
|
||||
).toEqual('/foo/bar');
|
||||
});
|
||||
|
||||
describe('absolute navigations', () => {
|
||||
|
|
@ -269,15 +273,14 @@ describe('createUrlTree', async () => {
|
|||
router.resetConfig([
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '**', outlet: 'left', component: class {}},
|
||||
],
|
||||
children: [{path: '**', outlet: 'left', component: class {}}],
|
||||
},
|
||||
]);
|
||||
await router.navigateByUrl('(left:search)');
|
||||
expect(router.url).toEqual('/(left:search)');
|
||||
expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString())
|
||||
.toEqual('/(left:projects/123)');
|
||||
expect(
|
||||
router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(),
|
||||
).toEqual('/(left:projects/123)');
|
||||
});
|
||||
it('empty path parent and sibling with a path', async () => {
|
||||
router.resetConfig([
|
||||
|
|
@ -291,22 +294,27 @@ describe('createUrlTree', async () => {
|
|||
]);
|
||||
await router.navigateByUrl('/x(left:search)');
|
||||
expect(router.url).toEqual('/x(left:search)');
|
||||
expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString())
|
||||
.toEqual('/x(left:projects/123)');
|
||||
expect(router
|
||||
.createUrlTree([
|
||||
'/', {
|
||||
outlets: {
|
||||
'primary': [{
|
||||
outlets: {
|
||||
'left': ['projects', '123'],
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
])
|
||||
.toString())
|
||||
.toEqual('/x(left:projects/123)');
|
||||
expect(
|
||||
router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(),
|
||||
).toEqual('/x(left:projects/123)');
|
||||
expect(
|
||||
router
|
||||
.createUrlTree([
|
||||
'/',
|
||||
{
|
||||
outlets: {
|
||||
'primary': [
|
||||
{
|
||||
outlets: {
|
||||
'left': ['projects', '123'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
.toString(),
|
||||
).toEqual('/x(left:projects/123)');
|
||||
});
|
||||
|
||||
it('empty path parent and sibling', async () => {
|
||||
|
|
@ -322,49 +330,52 @@ describe('createUrlTree', async () => {
|
|||
]);
|
||||
await router.navigateByUrl('/(left:search//right:define)');
|
||||
expect(router.url).toEqual('/(left:search//right:define)');
|
||||
expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString())
|
||||
.toEqual('/(left:projects/123//right:define)');
|
||||
expect(
|
||||
router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(),
|
||||
).toEqual('/(left:projects/123//right:define)');
|
||||
});
|
||||
it('two pathless parents', async () => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: '',
|
||||
children: [{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '**', outlet: 'left', component: class {}},
|
||||
]
|
||||
}],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
children: [{path: '**', outlet: 'left', component: class {}}],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
await router.navigateByUrl('(left:search)');
|
||||
expect(router.url).toEqual('/(left:search)');
|
||||
expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString())
|
||||
.toEqual('/(left:projects/123)');
|
||||
expect(
|
||||
router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(),
|
||||
).toEqual('/(left:projects/123)');
|
||||
});
|
||||
|
||||
it('maintains structure when primary outlet is not pathless', async () => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: '**', outlet: 'left', component: class {}},
|
||||
],
|
||||
children: [{path: '**', outlet: 'left', component: class {}}],
|
||||
},
|
||||
{path: '**', outlet: 'left', component: class {}},
|
||||
]);
|
||||
await router.navigateByUrl('/a/(left:search)');
|
||||
expect(router.url).toEqual('/a/(left:search)');
|
||||
expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString())
|
||||
.toEqual('/a/(left:search)(left:projects/123)');
|
||||
expect(
|
||||
router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(),
|
||||
).toEqual('/a/(left:search)(left:projects/123)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can navigate to nested route where commands is string', async () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = await createRoot(
|
||||
p, ['/', {outlets: {primary: ['child', {outlets: {primary: 'nested-primary'}}]}}]);
|
||||
const t = await createRoot(p, [
|
||||
'/',
|
||||
{outlets: {primary: ['child', {outlets: {primary: 'nested-primary'}}]}},
|
||||
]);
|
||||
expect(serializer.serialize(t)).toEqual('/child/nested-primary');
|
||||
});
|
||||
|
||||
|
|
@ -522,8 +533,9 @@ describe('createUrlTree', async () => {
|
|||
|
||||
it('should support updating secondary segments', async () => {
|
||||
await router.navigateByUrl('/a/b');
|
||||
const t =
|
||||
create(router.routerState.root.children[0].children[0], [{outlets: {right: ['c']}}]);
|
||||
const t = create(router.routerState.root.children[0].children[0], [
|
||||
{outlets: {right: ['c']}},
|
||||
]);
|
||||
expect(serializer.serialize(t)).toEqual('/a/b/(right:c)');
|
||||
});
|
||||
});
|
||||
|
|
@ -548,7 +560,13 @@ describe('createUrlTree', async () => {
|
|||
|
||||
it('should support pathless child of pathless root', async () => {
|
||||
router.resetConfig([
|
||||
{path: '', children: [{path: '', component: class {}}, {path: 'lazy', component: class {}}]}
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '', component: class {}},
|
||||
{path: 'lazy', component: class {}},
|
||||
],
|
||||
},
|
||||
]);
|
||||
await router.navigateByUrl('/');
|
||||
const t = create(router.routerState.root.children[0].children[0], ['lazy']);
|
||||
|
|
@ -557,117 +575,131 @@ describe('createUrlTree', async () => {
|
|||
});
|
||||
|
||||
async function createRoot(
|
||||
tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string): Promise<UrlTree> {
|
||||
tree: UrlTree,
|
||||
commands: any[],
|
||||
queryParams?: Params,
|
||||
fragment?: string,
|
||||
): Promise<UrlTree> {
|
||||
const router = TestBed.inject(Router);
|
||||
await router.navigateByUrl(tree);
|
||||
return router.createUrlTree(
|
||||
commands, {relativeTo: router.routerState.root, queryParams, fragment});
|
||||
return router.createUrlTree(commands, {
|
||||
relativeTo: router.routerState.root,
|
||||
queryParams,
|
||||
fragment,
|
||||
});
|
||||
}
|
||||
|
||||
function create(
|
||||
relativeTo: ActivatedRoute, commands: any[], queryParams?: Params, fragment?: string) {
|
||||
relativeTo: ActivatedRoute,
|
||||
commands: any[],
|
||||
queryParams?: Params,
|
||||
fragment?: string,
|
||||
) {
|
||||
return TestBed.inject(Router).createUrlTree(commands, {relativeTo, queryParams, fragment});
|
||||
}
|
||||
|
||||
describe('createUrlTreeFromSnapshot', async () => {
|
||||
it('can create a UrlTree relative to empty path named parent', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class MainPageComponent {
|
||||
constructor(private route: ActivatedRoute, private router: Router) {}
|
||||
@Component({
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class MainPageComponent {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
navigate() {
|
||||
this.router.navigateByUrl(
|
||||
createUrlTreeFromSnapshot(this.route.snapshot, ['innerRoute'], null, null));
|
||||
}
|
||||
}
|
||||
navigate() {
|
||||
this.router.navigateByUrl(
|
||||
createUrlTreeFromSnapshot(this.route.snapshot, ['innerRoute'], null, null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({template: 'child works!'})
|
||||
class ChildComponent {
|
||||
}
|
||||
@Component({template: 'child works!'})
|
||||
class ChildComponent {}
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet name="main-page"></router-outlet>',
|
||||
standalone: true,
|
||||
imports: [RouterModule]
|
||||
})
|
||||
class RootCmp {
|
||||
}
|
||||
@Component({
|
||||
template: '<router-outlet name="main-page"></router-outlet>',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class RootCmp {}
|
||||
|
||||
const routes: Routes = [{
|
||||
path: '',
|
||||
component: MainPageComponent,
|
||||
outlet: 'main-page',
|
||||
children: [{path: 'innerRoute', component: ChildComponent}]
|
||||
}];
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MainPageComponent,
|
||||
outlet: 'main-page',
|
||||
children: [{path: 'innerRoute', component: ChildComponent}],
|
||||
},
|
||||
];
|
||||
|
||||
TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
|
||||
router.initialNavigation();
|
||||
advance(fixture);
|
||||
fixture.debugElement.query(By.directive(MainPageComponent)).componentInstance.navigate();
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('child works!');
|
||||
}));
|
||||
router.initialNavigation();
|
||||
advance(fixture);
|
||||
fixture.debugElement.query(By.directive(MainPageComponent)).componentInstance.navigate();
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('child works!');
|
||||
}));
|
||||
|
||||
it('can navigate to relative to `ActivatedRouteSnapshot` in guard', fakeAsync(() => {
|
||||
@Injectable({providedIn: 'root'})
|
||||
class Guard {
|
||||
constructor(private readonly router: Router) {}
|
||||
canActivate(snapshot: ActivatedRouteSnapshot) {
|
||||
this.router.navigateByUrl(
|
||||
createUrlTreeFromSnapshot(snapshot, ['../sibling'], null, null));
|
||||
}
|
||||
}
|
||||
@Injectable({providedIn: 'root'})
|
||||
class Guard {
|
||||
constructor(private readonly router: Router) {}
|
||||
canActivate(snapshot: ActivatedRouteSnapshot) {
|
||||
this.router.navigateByUrl(createUrlTreeFromSnapshot(snapshot, ['../sibling'], null, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `main`,
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class GuardedComponent {
|
||||
}
|
||||
@Component({
|
||||
template: `main`,
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class GuardedComponent {}
|
||||
|
||||
@Component({template: 'sibling', standalone: true})
|
||||
class SiblingComponent {
|
||||
}
|
||||
@Component({template: 'sibling', standalone: true})
|
||||
class SiblingComponent {}
|
||||
|
||||
@Component(
|
||||
{template: '<router-outlet></router-outlet>', standalone: true, imports: [RouterModule]})
|
||||
class RootCmp {
|
||||
}
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
class RootCmp {}
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'parent',
|
||||
component: RootCmp,
|
||||
children: [
|
||||
{
|
||||
path: 'guarded',
|
||||
component: GuardedComponent,
|
||||
canActivate: [Guard],
|
||||
},
|
||||
{
|
||||
path: 'sibling',
|
||||
component: SiblingComponent,
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'parent',
|
||||
component: RootCmp,
|
||||
children: [
|
||||
{
|
||||
path: 'guarded',
|
||||
component: GuardedComponent,
|
||||
canActivate: [Guard],
|
||||
},
|
||||
{
|
||||
path: 'sibling',
|
||||
component: SiblingComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
|
||||
router.navigateByUrl('parent/guarded');
|
||||
advance(fixture);
|
||||
expect(router.url).toEqual('/parent/sibling');
|
||||
}));
|
||||
router.navigateByUrl('parent/guarded');
|
||||
advance(fixture);
|
||||
expect(router.url).toEqual('/parent/sibling');
|
||||
}));
|
||||
});
|
||||
|
||||
function advance(fixture: ComponentFixture<unknown>) {
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@ import {Component} from '@angular/core';
|
|||
template: 'default exported',
|
||||
selector: 'test-route',
|
||||
})
|
||||
export default class TestRoute {
|
||||
}
|
||||
export default class TestRoute {}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ import {Routes} from '@angular/router';
|
|||
template: 'default exported',
|
||||
selector: 'test-route',
|
||||
})
|
||||
export class TestRoute {
|
||||
}
|
||||
export class TestRoute {}
|
||||
|
||||
|
||||
export default [
|
||||
{path: '', pathMatch: 'full', component: TestRoute},
|
||||
] as Routes;
|
||||
export default [{path: '', pathMatch: 'full', component: TestRoute}] as Routes;
|
||||
|
|
|
|||
|
|
@ -9,185 +9,190 @@
|
|||
import {CommonModule, NgForOf} from '@angular/common';
|
||||
import {Component, Input, Type} from '@angular/core';
|
||||
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {provideRouter, Router, RouterModule, RouterOutlet, withComponentInputBinding} from '@angular/router/src';
|
||||
import {
|
||||
provideRouter,
|
||||
Router,
|
||||
RouterModule,
|
||||
RouterOutlet,
|
||||
withComponentInputBinding,
|
||||
} from '@angular/router/src';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
|
||||
|
||||
describe('router outlet name', () => {
|
||||
it('should support name binding', fakeAsync(() => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<router-outlet [name]="name"></router-outlet>',
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
class RootCmp {
|
||||
name = 'popup';
|
||||
}
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<router-outlet [name]="name"></router-outlet>',
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
class RootCmp {
|
||||
name = 'popup';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'popup component',
|
||||
standalone: true,
|
||||
})
|
||||
class PopupCmp {
|
||||
}
|
||||
@Component({
|
||||
template: 'popup component',
|
||||
standalone: true,
|
||||
})
|
||||
class PopupCmp {}
|
||||
|
||||
TestBed.configureTestingModule(
|
||||
{imports: [RouterModule.forRoot([{path: '', outlet: 'popup', component: PopupCmp}])]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('popup component');
|
||||
}));
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: '', outlet: 'popup', component: PopupCmp}])],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('popup component');
|
||||
}));
|
||||
|
||||
it('should be able to change the name of the outlet', fakeAsync(() => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<router-outlet [name]="name"></router-outlet>',
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
class RootCmp {
|
||||
name = '';
|
||||
}
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<router-outlet [name]="name"></router-outlet>',
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
class RootCmp {
|
||||
name = '';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'hello world',
|
||||
standalone: true,
|
||||
})
|
||||
class GreetingCmp {
|
||||
}
|
||||
@Component({
|
||||
template: 'hello world',
|
||||
standalone: true,
|
||||
})
|
||||
class GreetingCmp {}
|
||||
|
||||
@Component({
|
||||
template: 'goodbye cruel world',
|
||||
standalone: true,
|
||||
})
|
||||
class FarewellCmp {
|
||||
}
|
||||
@Component({
|
||||
template: 'goodbye cruel world',
|
||||
standalone: true,
|
||||
})
|
||||
class FarewellCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{path: '', outlet: 'greeting', component: GreetingCmp},
|
||||
{path: '', outlet: 'farewell', component: FarewellCmp},
|
||||
])]
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: '', outlet: 'greeting', component: GreetingCmp},
|
||||
{path: '', outlet: 'farewell', component: FarewellCmp},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('hello');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('hello');
|
||||
|
||||
fixture.componentInstance.name = 'greeting';
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('hello');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye');
|
||||
fixture.componentInstance.name = 'greeting';
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('hello');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye');
|
||||
|
||||
fixture.componentInstance.name = 'goodbye';
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('goodbye');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('hello');
|
||||
}));
|
||||
fixture.componentInstance.name = 'goodbye';
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('goodbye');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('hello');
|
||||
}));
|
||||
|
||||
it('should support outlets in ngFor', fakeAsync(() => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<div *ngFor="let outlet of outlets">
|
||||
<router-outlet [name]="outlet"></router-outlet>
|
||||
</div>
|
||||
`,
|
||||
imports: [RouterOutlet, NgForOf],
|
||||
})
|
||||
class RootCmp {
|
||||
outlets = ['outlet1', 'outlet2', 'outlet3'];
|
||||
}
|
||||
imports: [RouterOutlet, NgForOf],
|
||||
})
|
||||
class RootCmp {
|
||||
outlets = ['outlet1', 'outlet2', 'outlet3'];
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'component 1',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp1 {
|
||||
}
|
||||
@Component({
|
||||
template: 'component 1',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp1 {}
|
||||
|
||||
@Component({
|
||||
template: 'component 2',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp2 {
|
||||
}
|
||||
@Component({
|
||||
template: 'component 2',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp2 {}
|
||||
|
||||
@Component({
|
||||
template: 'component 3',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp3 {
|
||||
}
|
||||
@Component({
|
||||
template: 'component 3',
|
||||
standalone: true,
|
||||
})
|
||||
class Cmp3 {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{path: '1', outlet: 'outlet1', component: Cmp1},
|
||||
{path: '2', outlet: 'outlet2', component: Cmp2},
|
||||
{path: '3', outlet: 'outlet3', component: Cmp3},
|
||||
])]
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: '1', outlet: 'outlet1', component: Cmp1},
|
||||
{path: '2', outlet: 'outlet2', component: Cmp2},
|
||||
{path: '3', outlet: 'outlet3', component: Cmp3},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.navigate([{outlets: {'outlet1': '1'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('component 1');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 2');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 3');
|
||||
router.navigate([{outlets: {'outlet1': '1'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('component 1');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 2');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 3');
|
||||
|
||||
router.navigate([{outlets: {'outlet1': null, 'outlet2': '2', 'outlet3': '3'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 1');
|
||||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 2.*component 3');
|
||||
router.navigate([{outlets: {'outlet1': null, 'outlet2': '2', 'outlet3': '3'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('component 1');
|
||||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 2.*component 3');
|
||||
|
||||
// reverse the outlets
|
||||
fixture.componentInstance.outlets = ['outlet3', 'outlet2', 'outlet1'];
|
||||
router.navigate([{outlets: {'outlet1': '1', 'outlet2': '2', 'outlet3': '3'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 3.*component 2.*component 1');
|
||||
}));
|
||||
// reverse the outlets
|
||||
fixture.componentInstance.outlets = ['outlet3', 'outlet2', 'outlet1'];
|
||||
router.navigate([{outlets: {'outlet1': '1', 'outlet2': '2', 'outlet3': '3'}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 3.*component 2.*component 1');
|
||||
}));
|
||||
|
||||
it('should not activate if route is changed', fakeAsync(() => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div *ngIf="initDone"><router-outlet></router-outlet></div>',
|
||||
imports: [RouterOutlet, CommonModule],
|
||||
})
|
||||
class ParentCmp {
|
||||
initDone = false;
|
||||
constructor() {
|
||||
setTimeout(() => this.initDone = true, 1000);
|
||||
}
|
||||
}
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div *ngIf="initDone"><router-outlet></router-outlet></div>',
|
||||
imports: [RouterOutlet, CommonModule],
|
||||
})
|
||||
class ParentCmp {
|
||||
initDone = false;
|
||||
constructor() {
|
||||
setTimeout(() => (this.initDone = true), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'child component',
|
||||
standalone: true,
|
||||
})
|
||||
class ChildCmp {
|
||||
}
|
||||
@Component({
|
||||
template: 'child component',
|
||||
standalone: true,
|
||||
})
|
||||
class ChildCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{path: 'parent', component: ParentCmp, children: [{path: 'child', component: ChildCmp}]}
|
||||
])]
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, ParentCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: 'parent', component: ParentCmp, children: [{path: 'child', component: ChildCmp}]},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, ParentCmp);
|
||||
|
||||
advance(fixture, 250);
|
||||
router.navigate(['parent/child']);
|
||||
advance(fixture, 250);
|
||||
// Not contain because initDone is still false
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('child component');
|
||||
advance(fixture, 250);
|
||||
router.navigate(['parent/child']);
|
||||
advance(fixture, 250);
|
||||
// Not contain because initDone is still false
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('child component');
|
||||
|
||||
advance(fixture, 1500);
|
||||
router.navigate(['parent']);
|
||||
advance(fixture, 1500);
|
||||
// Not contain because route was changed back to parent
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('child component');
|
||||
}));
|
||||
advance(fixture, 1500);
|
||||
router.navigate(['parent']);
|
||||
advance(fixture, 1500);
|
||||
// Not contain because route was changed back to parent
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('child component');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('component input binding', () => {
|
||||
|
|
@ -200,8 +205,9 @@ describe('component input binding', () => {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers:
|
||||
[provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())]
|
||||
providers: [
|
||||
provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
|
|
@ -228,14 +234,19 @@ describe('component input binding', () => {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(
|
||||
[{
|
||||
path: '**',
|
||||
component: MyComponent,
|
||||
data: {'dataA': 'My static data'},
|
||||
resolve: {'resolveA': () => 'My resolved data'},
|
||||
}],
|
||||
withComponentInputBinding())]
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: '**',
|
||||
component: MyComponent,
|
||||
data: {'dataA': 'My static data'},
|
||||
resolve: {'resolveA': () => 'My resolved data'},
|
||||
},
|
||||
],
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
|
|
@ -253,8 +264,9 @@ describe('component input binding', () => {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers:
|
||||
[provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())]
|
||||
providers: [
|
||||
provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
|
|
@ -262,59 +274,66 @@ describe('component input binding', () => {
|
|||
expect(instance.language).toEqual('english');
|
||||
});
|
||||
|
||||
it('when keys conflict, sets inputs based on priority: data > path params > query params',
|
||||
async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
class MyComponent {
|
||||
@Input() result?: string;
|
||||
}
|
||||
it('when keys conflict, sets inputs based on priority: data > path params > query params', async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
class MyComponent {
|
||||
@Input() result?: string;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'withData',
|
||||
component: MyComponent,
|
||||
data: {'result': 'from data'},
|
||||
},
|
||||
{
|
||||
path: 'withoutData',
|
||||
component: MyComponent,
|
||||
},
|
||||
],
|
||||
withComponentInputBinding())]
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'withData',
|
||||
component: MyComponent,
|
||||
data: {'result': 'from data'},
|
||||
},
|
||||
{
|
||||
path: 'withoutData',
|
||||
component: MyComponent,
|
||||
},
|
||||
],
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
let instance = await harness.navigateByUrl(
|
||||
'/withData;result=from path param?result=from query params', MyComponent);
|
||||
expect(instance.result).toEqual('from data');
|
||||
let instance = await harness.navigateByUrl(
|
||||
'/withData;result=from path param?result=from query params',
|
||||
MyComponent,
|
||||
);
|
||||
expect(instance.result).toEqual('from data');
|
||||
|
||||
// Same component, different instance because it's a different route
|
||||
instance = await harness.navigateByUrl(
|
||||
'/withoutData;result=from path param?result=from query params', MyComponent);
|
||||
expect(instance.result).toEqual('from path param');
|
||||
instance = await harness.navigateByUrl('/withoutData?result=from query params', MyComponent);
|
||||
expect(instance.result).toEqual('from query params');
|
||||
});
|
||||
// Same component, different instance because it's a different route
|
||||
instance = await harness.navigateByUrl(
|
||||
'/withoutData;result=from path param?result=from query params',
|
||||
MyComponent,
|
||||
);
|
||||
expect(instance.result).toEqual('from path param');
|
||||
instance = await harness.navigateByUrl('/withoutData?result=from query params', MyComponent);
|
||||
expect(instance.result).toEqual('from query params');
|
||||
});
|
||||
|
||||
it('does not write multiple times if two sources of conflicting keys both update', async () => {
|
||||
let resultLog: Array<string|undefined> = [];
|
||||
let resultLog: Array<string | undefined> = [];
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
class MyComponent {
|
||||
@Input()
|
||||
set result(v: string|undefined) {
|
||||
set result(v: string | undefined) {
|
||||
resultLog.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers:
|
||||
[provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())]
|
||||
providers: [
|
||||
provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
|
|
@ -339,19 +358,21 @@ describe('component input binding', () => {
|
|||
imports: [RouterOutlet],
|
||||
standalone: true,
|
||||
})
|
||||
class OutletWrapper {
|
||||
}
|
||||
class OutletWrapper {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(
|
||||
[{
|
||||
path: 'root',
|
||||
component: OutletWrapper,
|
||||
children: [
|
||||
{path: '**', component: MyComponent},
|
||||
]
|
||||
}],
|
||||
withComponentInputBinding())]
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'root',
|
||||
component: OutletWrapper,
|
||||
children: [{path: '**', component: MyComponent}],
|
||||
},
|
||||
],
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create('/root/child?myInput=1');
|
||||
expect(harness.routeNativeElement!.innerText).toBe('1');
|
||||
|
|
|
|||
|
|
@ -27,24 +27,32 @@ export function provideTokenLogger(token: string, returnValue = true as boolean
|
|||
return {
|
||||
provide: token,
|
||||
useFactory: (logger: Logger) => () => (logger.add(token), returnValue),
|
||||
deps: [Logger]
|
||||
deps: [Logger],
|
||||
};
|
||||
}
|
||||
|
||||
export declare type ARSArgs = {
|
||||
url?: UrlSegment[],
|
||||
params?: Params,
|
||||
queryParams?: Params,
|
||||
fragment?: string,
|
||||
data?: Data,
|
||||
outlet?: string, component: Type<unknown>| string | null,
|
||||
routeConfig?: Route | null,
|
||||
resolve?: ResolveData
|
||||
url?: UrlSegment[];
|
||||
params?: Params;
|
||||
queryParams?: Params;
|
||||
fragment?: string;
|
||||
data?: Data;
|
||||
outlet?: string;
|
||||
component: Type<unknown> | string | null;
|
||||
routeConfig?: Route | null;
|
||||
resolve?: ResolveData;
|
||||
};
|
||||
|
||||
export function createActivatedRouteSnapshot(args: ARSArgs): ActivatedRouteSnapshot {
|
||||
return new (ActivatedRouteSnapshot as any)(
|
||||
args.url || [], args.params || {}, args.queryParams || null, args.fragment || null,
|
||||
args.data || null, args.outlet || null, args.component, args.routeConfig || {},
|
||||
args.resolve || {});
|
||||
args.url || [],
|
||||
args.params || {},
|
||||
args.queryParams || null,
|
||||
args.fragment || null,
|
||||
args.data || null,
|
||||
args.outlet || null,
|
||||
args.component,
|
||||
args.routeConfig || {},
|
||||
args.resolve || {},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,6 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {TestScheduler} from 'rxjs/testing';
|
||||
|
|
@ -14,7 +13,6 @@ import {TestScheduler} from 'rxjs/testing';
|
|||
import {prioritizedGuardValue} from '../../src/operators/prioritized_guard_value';
|
||||
import {Router} from '../../src/router';
|
||||
|
||||
|
||||
describe('prioritizedGuardValue operator', () => {
|
||||
let testScheduler: TestScheduler;
|
||||
let router: Router;
|
||||
|
|
@ -39,11 +37,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------T--';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -56,11 +54,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------F--';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -76,11 +74,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' ------------T---';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -98,11 +96,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------U---';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -120,11 +118,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------F---';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -142,11 +140,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------U----';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -166,11 +164,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------U---';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
urlLookup,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -187,11 +185,11 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' -------------T---';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
resultLookup,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
resultLookup,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -204,17 +202,15 @@ describe('prioritizedGuardValue operator', () => {
|
|||
|
||||
const expected = ' ---------#';
|
||||
|
||||
expectObservable(source.pipe(prioritizedGuardValue()))
|
||||
.toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */);
|
||||
expectObservable(source.pipe(prioritizedGuardValue())).toBe(
|
||||
expected,
|
||||
TF,
|
||||
/* an error here maybe */
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
function assertDeepEquals(a: any, b: any) {
|
||||
return expect(a).toEqual(b);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,32 +15,35 @@ import {EMPTY, interval, NEVER, of} from 'rxjs';
|
|||
describe('resolveData operator', () => {
|
||||
it('should take only the first emitted value of every resolver', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => interval()}}])]
|
||||
providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => interval()}}])],
|
||||
});
|
||||
await RouterTestingHarness.create('/');
|
||||
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({e1: 0});
|
||||
});
|
||||
|
||||
it('should cancel navigation if a resolver does not complete', async () => {
|
||||
TestBed.configureTestingModule(
|
||||
{providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => EMPTY}}])]});
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => EMPTY}}])],
|
||||
});
|
||||
await RouterTestingHarness.create('/a');
|
||||
expect(TestBed.inject(Router).url).toEqual('/');
|
||||
});
|
||||
|
||||
it('should cancel navigation if 1 of 2 resolvers does not emit', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers:
|
||||
[provideRouter([{path: '**', children: [], resolve: {e0: () => of(0), e1: () => EMPTY}}])]
|
||||
providers: [
|
||||
provideRouter([{path: '**', children: [], resolve: {e0: () => of(0), e1: () => EMPTY}}]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/a');
|
||||
expect(TestBed.inject(Router).url).toEqual('/');
|
||||
});
|
||||
|
||||
it('should complete instantly if at least one resolver doesn\'t emit', async () => {
|
||||
it("should complete instantly if at least one resolver doesn't emit", async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers:
|
||||
[provideRouter([{path: '**', children: [], resolve: {e0: () => EMPTY, e1: () => NEVER}}])]
|
||||
providers: [
|
||||
provideRouter([{path: '**', children: [], resolve: {e0: () => EMPTY, e1: () => NEVER}}]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/a');
|
||||
expect(TestBed.inject(Router).url).toEqual('/');
|
||||
|
|
@ -50,29 +53,35 @@ describe('resolveData operator', () => {
|
|||
let value = 0;
|
||||
let bValue = 0;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
runGuardsAndResolvers: () => false,
|
||||
children: [{
|
||||
path: '',
|
||||
resolve: {d0: () => ++value},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [],
|
||||
}],
|
||||
},
|
||||
{
|
||||
path: 'b',
|
||||
outlet: 'aux',
|
||||
runGuardsAndResolvers: () => false,
|
||||
children: [{
|
||||
path: '',
|
||||
resolve: {d1: () => ++bValue},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [],
|
||||
}]
|
||||
},
|
||||
])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
runGuardsAndResolvers: () => false,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
resolve: {d0: () => ++value},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'b',
|
||||
outlet: 'aux',
|
||||
runGuardsAndResolvers: () => false,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
resolve: {d1: () => ++bValue},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const harness = await RouterTestingHarness.create('/a(aux:b)');
|
||||
|
|
@ -87,43 +96,52 @@ describe('resolveData operator', () => {
|
|||
it('should update children inherited data when resolvers run', async () => {
|
||||
let value = 0;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{
|
||||
path: 'a',
|
||||
children: [{path: 'b', children: []}],
|
||||
resolve: {d0: () => ++value},
|
||||
runGuardsAndResolvers: 'always',
|
||||
}])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
children: [{path: 'b', children: []}],
|
||||
resolve: {d0: () => ++value},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create('/a/b');
|
||||
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 1});
|
||||
expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({
|
||||
d0: 1
|
||||
d0: 1,
|
||||
});
|
||||
|
||||
await harness.navigateByUrl('/a/b#new');
|
||||
expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 2});
|
||||
expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({
|
||||
d0: 2
|
||||
d0: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct data when parent resolver runs but data is not inherited', async () => {
|
||||
@Component({template: ''})
|
||||
class Empty {
|
||||
}
|
||||
class Empty {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{
|
||||
path: 'a',
|
||||
component: Empty,
|
||||
data: {parent: 'parent'},
|
||||
resolve: {other: () => 'other'},
|
||||
children: [{
|
||||
path: 'b',
|
||||
data: {child: 'child'},
|
||||
component: Empty,
|
||||
}]
|
||||
}])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
component: Empty,
|
||||
data: {parent: 'parent'},
|
||||
resolve: {other: () => 'other'},
|
||||
children: [
|
||||
{
|
||||
path: 'b',
|
||||
data: {child: 'child'},
|
||||
component: Empty,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/a/b');
|
||||
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
|
||||
|
|
@ -133,22 +151,27 @@ describe('resolveData operator', () => {
|
|||
|
||||
it('should have static title when there is a resolver', async () => {
|
||||
@Component({template: ''})
|
||||
class Empty {
|
||||
}
|
||||
class Empty {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{
|
||||
path: 'a',
|
||||
title: 'a title',
|
||||
component: Empty,
|
||||
resolve: {other: () => 'other'},
|
||||
children: [{
|
||||
path: 'b',
|
||||
title: 'b title',
|
||||
component: Empty,
|
||||
resolve: {otherb: () => 'other b'},
|
||||
}]
|
||||
}])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
title: 'a title',
|
||||
component: Empty,
|
||||
resolve: {other: () => 'other'},
|
||||
children: [
|
||||
{
|
||||
path: 'b',
|
||||
title: 'b title',
|
||||
component: Empty,
|
||||
resolve: {otherb: () => 'other b'},
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/a/b');
|
||||
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
|
||||
|
|
@ -158,16 +181,24 @@ describe('resolveData operator', () => {
|
|||
|
||||
it('should inherit resolved data from parent of parent route', async () => {
|
||||
@Component({template: ''})
|
||||
class Empty {
|
||||
}
|
||||
class Empty {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{
|
||||
path: 'a',
|
||||
resolve: {aResolve: () => 'a'},
|
||||
children:
|
||||
[{path: 'b', resolve: {bResolve: () => 'b'}, children: [{path: 'c', component: Empty}]}]
|
||||
}])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: 'a',
|
||||
resolve: {aResolve: () => 'a'},
|
||||
children: [
|
||||
{
|
||||
path: 'b',
|
||||
resolve: {bResolve: () => 'b'},
|
||||
children: [{path: 'c', component: Empty}],
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/a/b/c');
|
||||
const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,16 @@ import {provideLocationMocks} from '@angular/common/testing';
|
|||
import {Component, inject, Inject, Injectable, NgModule} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {ActivatedRoute, provideRouter, ResolveFn, Router, RouterModule, RouterStateSnapshot, TitleStrategy, withRouterConfig} from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
provideRouter,
|
||||
ResolveFn,
|
||||
Router,
|
||||
RouterModule,
|
||||
RouterStateSnapshot,
|
||||
TitleStrategy,
|
||||
withRouterConfig,
|
||||
} from '@angular/router';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
|
||||
describe('title strategy', () => {
|
||||
|
|
@ -21,87 +30,85 @@ describe('title strategy', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TestModule,
|
||||
],
|
||||
imports: [TestModule],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
provideRouter([], withRouterConfig({paramsInheritanceStrategy: 'always'})),
|
||||
]
|
||||
],
|
||||
});
|
||||
router = TestBed.inject(Router);
|
||||
document = TestBed.inject(DOCUMENT);
|
||||
});
|
||||
|
||||
it('sets page title from data', fakeAsync(() => {
|
||||
router.resetConfig([{path: 'home', title: 'My Application', component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('My Application');
|
||||
}));
|
||||
router.resetConfig([{path: 'home', title: 'My Application', component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('My Application');
|
||||
}));
|
||||
|
||||
it('sets page title from resolved data', fakeAsync(() => {
|
||||
router.resetConfig([{path: 'home', title: TitleResolver, component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
router.resetConfig([{path: 'home', title: TitleResolver, component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
|
||||
it('sets page title from resolved data function', fakeAsync(() => {
|
||||
router.resetConfig([{path: 'home', title: () => 'resolved title', component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
router.resetConfig([{path: 'home', title: () => 'resolved title', component: BlankCmp}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
|
||||
it('sets title with child routes', fakeAsync(() => {
|
||||
router.resetConfig([{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [
|
||||
{path: '', title: 'child title', component: BlankCmp},
|
||||
]
|
||||
}]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('child title');
|
||||
}));
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [{path: '', title: 'child title', component: BlankCmp}],
|
||||
},
|
||||
]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('child title');
|
||||
}));
|
||||
|
||||
it('sets title with child routes and named outlets', fakeAsync(() => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [
|
||||
{path: '', title: 'child title', component: BlankCmp},
|
||||
{path: '', outlet: 'childaux', title: 'child aux title', component: BlankCmp},
|
||||
],
|
||||
},
|
||||
{path: 'compose', component: BlankCmp, outlet: 'aux', title: 'compose'}
|
||||
]);
|
||||
router.navigateByUrl('home(aux:compose)');
|
||||
tick();
|
||||
expect(document.title).toBe('child title');
|
||||
}));
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [
|
||||
{path: '', title: 'child title', component: BlankCmp},
|
||||
{path: '', outlet: 'childaux', title: 'child aux title', component: BlankCmp},
|
||||
],
|
||||
},
|
||||
{path: 'compose', component: BlankCmp, outlet: 'aux', title: 'compose'},
|
||||
]);
|
||||
router.navigateByUrl('home(aux:compose)');
|
||||
tick();
|
||||
expect(document.title).toBe('child title');
|
||||
}));
|
||||
|
||||
it('sets page title with inherited params', fakeAsync(() => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
title: TitleResolver,
|
||||
component: BlankCmp,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'My Application',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
title: TitleResolver,
|
||||
component: BlankCmp,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toBe('resolved title');
|
||||
}));
|
||||
|
||||
it('can get the title from the ActivatedRouteSnapshot', async () => {
|
||||
router.resetConfig([
|
||||
|
|
@ -122,16 +129,18 @@ describe('title strategy', () => {
|
|||
title?: string;
|
||||
|
||||
constructor() {
|
||||
this.title$.subscribe(v => this.title = v);
|
||||
this.title$.subscribe((v) => (this.title = v));
|
||||
}
|
||||
}
|
||||
const titleResolver: ResolveFn<string> = route => route.queryParams['id'];
|
||||
router.resetConfig([{
|
||||
path: 'home',
|
||||
title: titleResolver,
|
||||
component: HomeCmp,
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange'
|
||||
}]);
|
||||
const titleResolver: ResolveFn<string> = (route) => route.queryParams['id'];
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: titleResolver,
|
||||
component: HomeCmp,
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
|
||||
},
|
||||
]);
|
||||
|
||||
const harness = await RouterTestingHarness.create();
|
||||
const homeCmp = await harness.navigateByUrl('/home?id=1', HomeCmp);
|
||||
|
|
@ -143,68 +152,60 @@ describe('title strategy', () => {
|
|||
|
||||
describe('custom strategies', () => {
|
||||
it('overriding the setTitle method', fakeAsync(() => {
|
||||
@Injectable({providedIn: 'root'})
|
||||
class TemplatePageTitleStrategy extends TitleStrategy {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {
|
||||
super();
|
||||
}
|
||||
@Injectable({providedIn: 'root'})
|
||||
class TemplatePageTitleStrategy extends TitleStrategy {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Example of how setTitle could implement a template for the title
|
||||
override updateTitle(state: RouterStateSnapshot) {
|
||||
const title = this.buildTitle(state);
|
||||
this.document.title = `My Application | ${title}`;
|
||||
}
|
||||
}
|
||||
// Example of how setTitle could implement a template for the title
|
||||
override updateTitle(state: RouterStateSnapshot) {
|
||||
const title = this.buildTitle(state);
|
||||
this.document.title = `My Application | ${title}`;
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TestModule,
|
||||
],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
provideRouter([]),
|
||||
{provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
|
||||
]
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const document = TestBed.inject(DOCUMENT);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'Home',
|
||||
children: [
|
||||
{path: '', title: 'Child', component: BlankCmp},
|
||||
],
|
||||
},
|
||||
]);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TestModule],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
provideRouter([]),
|
||||
{provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const document = TestBed.inject(DOCUMENT);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'home',
|
||||
title: 'Home',
|
||||
children: [{path: '', title: 'Child', component: BlankCmp}],
|
||||
},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toEqual('My Application | Child');
|
||||
}));
|
||||
router.navigateByUrl('home');
|
||||
tick();
|
||||
expect(document.title).toEqual('My Application | Child');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@Component({template: ''})
|
||||
export class BlankCmp {
|
||||
}
|
||||
export class BlankCmp {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet name="aux"></router-outlet>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class RootCmp {
|
||||
}
|
||||
export class RootCmp {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [BlankCmp],
|
||||
imports: [RouterModule.forRoot([])],
|
||||
})
|
||||
export class TestModule {
|
||||
}
|
||||
|
||||
export class TestModule {}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class TitleResolver {
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ describe('recognize', async () => {
|
|||
});
|
||||
|
||||
it('should freeze params object', async () => {
|
||||
const s: RouterStateSnapshot =
|
||||
await recognize([{path: 'a/:id', component: ComponentA}], 'a/10');
|
||||
const s: RouterStateSnapshot = await recognize(
|
||||
[{path: 'a/:id', component: ComponentA}],
|
||||
'a/10',
|
||||
);
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
const child = s.root.firstChild!;
|
||||
expect(Object.isFrozen(child.params)).toBeTruthy();
|
||||
|
|
@ -33,8 +35,10 @@ describe('recognize', async () => {
|
|||
|
||||
it('should freeze data object (but not original route data)', async () => {
|
||||
const someData = {a: 1};
|
||||
const s: RouterStateSnapshot =
|
||||
await recognize([{path: '**', component: ComponentA, data: someData}], 'a');
|
||||
const s: RouterStateSnapshot = await recognize(
|
||||
[{path: '**', component: ComponentA, data: someData}],
|
||||
'a',
|
||||
);
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
const child = s.root.firstChild!;
|
||||
expect(Object.isFrozen(child.data)).toBeTruthy();
|
||||
|
|
@ -43,11 +47,13 @@ describe('recognize', async () => {
|
|||
|
||||
it('should support secondary routes', async () => {
|
||||
const s: RouterStateSnapshot = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)');
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'},
|
||||
],
|
||||
'a(left:b//right:c)',
|
||||
);
|
||||
const c = s.root.children;
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'left');
|
||||
|
|
@ -57,11 +63,13 @@ describe('recognize', async () => {
|
|||
it('should set url segment and index properly', async () => {
|
||||
const url = tree('a(left:b//right:c)');
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)');
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'},
|
||||
],
|
||||
'a(left:b//right:c)',
|
||||
);
|
||||
expect(s.root.url.toString()).toEqual(url.root.toString());
|
||||
|
||||
const c = s.root.children;
|
||||
|
|
@ -74,28 +82,36 @@ describe('recognize', async () => {
|
|||
|
||||
it('should match routes in the depth first order', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
|
||||
{path: 'a/:id', component: ComponentC}
|
||||
],
|
||||
'a/paramA');
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
|
||||
{path: 'a/:id', component: ComponentC},
|
||||
],
|
||||
'a/paramA',
|
||||
);
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
checkActivatedRoute(s.root.firstChild!.firstChild!, 'paramA', {id: 'paramA'}, ComponentB);
|
||||
|
||||
const s2 = await recognize(
|
||||
[{path: 'a', component: ComponentA}, {path: 'a/:id', component: ComponentC}], 'a/paramA');
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'a/:id', component: ComponentC},
|
||||
],
|
||||
'a/paramA',
|
||||
);
|
||||
checkActivatedRoute(s2.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s2.root.firstChild!, 'a/paramA', {id: 'paramA'}, ComponentC);
|
||||
});
|
||||
|
||||
it('should use outlet name when matching secondary routes', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'b', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(right:b)');
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'b', component: ComponentC, outlet: 'right'},
|
||||
],
|
||||
'a(right:b)',
|
||||
);
|
||||
const c = s.root.children;
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentC, 'right');
|
||||
|
|
@ -103,16 +119,18 @@ describe('recognize', async () => {
|
|||
|
||||
it('should handle non top-level secondary routes', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC, outlet: 'left'}
|
||||
]
|
||||
},
|
||||
],
|
||||
'a/(b//left:c)');
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'},
|
||||
],
|
||||
},
|
||||
],
|
||||
'a/(b//left:c)',
|
||||
);
|
||||
const c = s.root.firstChild!.children;
|
||||
checkActivatedRoute(c[0], 'b', {}, ComponentB, PRIMARY_OUTLET);
|
||||
checkActivatedRoute(c[1], 'c', {}, ComponentC, 'left');
|
||||
|
|
@ -120,11 +138,13 @@ describe('recognize', async () => {
|
|||
|
||||
it('should sort routes by outlet name', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'c'},
|
||||
{path: 'b', component: ComponentB, outlet: 'b'}
|
||||
],
|
||||
'a(c:c//b:b)');
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'c', component: ComponentC, outlet: 'c'},
|
||||
{path: 'b', component: ComponentB, outlet: 'b'},
|
||||
],
|
||||
'a(c:c//b:b)',
|
||||
);
|
||||
const c = s.root.children;
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'b');
|
||||
|
|
@ -133,11 +153,12 @@ describe('recognize', async () => {
|
|||
|
||||
it('should support matrix parameters', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
],
|
||||
'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)');
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'},
|
||||
],
|
||||
'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)',
|
||||
);
|
||||
const c = s.root.children;
|
||||
checkActivatedRoute(c[0], 'a', {a1: '11', a2: '22'}, ComponentA);
|
||||
checkActivatedRoute(c[0].firstChild!, 'b', {b1: '111', b2: '222'}, ComponentB);
|
||||
|
|
@ -151,60 +172,75 @@ describe('recognize', async () => {
|
|||
expect(r.data).toEqual({one: 1});
|
||||
});
|
||||
|
||||
it('should inherit componentless route\'s data', async () => {
|
||||
it("should inherit componentless route's data", async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
data: {one: 1},
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
|
||||
}],
|
||||
'a/b');
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
);
|
||||
const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!;
|
||||
expect(r.data).toEqual({one: 1, two: 2});
|
||||
});
|
||||
|
||||
it('should not inherit route\'s data if it has component', async () => {
|
||||
it("should not inherit route's data if it has component", async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
data: {one: 1},
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
|
||||
}],
|
||||
'a/b');
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
);
|
||||
const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!;
|
||||
expect(r.data).toEqual({two: 2});
|
||||
});
|
||||
|
||||
it('should not inherit route\'s data if it has loadComponent', async () => {
|
||||
it("should not inherit route's data if it has loadComponent", async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
loadComponent: () => ComponentA,
|
||||
data: {one: 1},
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
|
||||
}],
|
||||
'a/b');
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
);
|
||||
const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!;
|
||||
expect(r.data).toEqual({two: 2});
|
||||
});
|
||||
|
||||
it('should inherit route\'s data if paramsInheritanceStrategy is \'always\'', async () => {
|
||||
it("should inherit route's data if paramsInheritanceStrategy is 'always'", async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
data: {one: 1},
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
|
||||
}],
|
||||
'a/b', 'always');
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
'always',
|
||||
);
|
||||
const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!;
|
||||
expect(r.data).toEqual({one: 1, two: 2});
|
||||
});
|
||||
|
||||
it('should set resolved data', async () => {
|
||||
const s =
|
||||
await recognize([{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a');
|
||||
const s = await recognize(
|
||||
[{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}],
|
||||
'a',
|
||||
);
|
||||
const r: any = s.root.firstChild!;
|
||||
expect(r._resolve).toEqual({one: 'some-token'});
|
||||
});
|
||||
|
|
@ -224,20 +260,26 @@ describe('recognize', async () => {
|
|||
|
||||
it('should work (nested case)', async () => {
|
||||
const s = await recognize(
|
||||
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '');
|
||||
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}],
|
||||
'',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, '', {}, ComponentA);
|
||||
checkActivatedRoute(s.root.firstChild!.firstChild!, '', {}, ComponentB);
|
||||
});
|
||||
|
||||
it('should inherit params', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children:
|
||||
[{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}]
|
||||
}],
|
||||
'/a;p=1');
|
||||
children: [
|
||||
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]},
|
||||
],
|
||||
},
|
||||
],
|
||||
'/a;p=1',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {p: '1'}, ComponentA);
|
||||
checkActivatedRoute(s.root.firstChild!.firstChild!, '', {p: '1'}, ComponentB);
|
||||
checkActivatedRoute(s.root.firstChild!.firstChild!.firstChild!, '', {p: '1'}, ComponentC);
|
||||
|
|
@ -247,14 +289,18 @@ describe('recognize', async () => {
|
|||
describe('aux split is in the middle', async () => {
|
||||
it('should match (non-terminal)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB}, {path: '', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b');
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'},
|
||||
],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -262,49 +308,53 @@ describe('recognize', async () => {
|
|||
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
|
||||
});
|
||||
|
||||
it('should match (non-terminal) when both primary and secondary and primary has a child',
|
||||
async () => {
|
||||
const config = [{
|
||||
path: 'parent',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: ComponentD,
|
||||
outlet: 'secondary',
|
||||
}
|
||||
]
|
||||
}];
|
||||
it('should match (non-terminal) when both primary and secondary and primary has a child', async () => {
|
||||
const config = [
|
||||
{
|
||||
path: 'parent',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: ComponentD,
|
||||
outlet: 'secondary',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const s = await recognize(config, 'parent/b');
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.root.firstChild!, 'parent', {}, null);
|
||||
const s = await recognize(config, 'parent/b');
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.root.firstChild!, 'parent', {}, null);
|
||||
|
||||
const cc = s.root.firstChild!.children;
|
||||
checkActivatedRoute(cc[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary');
|
||||
const cc = s.root.firstChild!.children;
|
||||
checkActivatedRoute(cc[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary');
|
||||
|
||||
checkActivatedRoute(cc[0].firstChild!, 'b', {}, ComponentB);
|
||||
});
|
||||
checkActivatedRoute(cc[0].firstChild!, 'b', {}, ComponentB);
|
||||
});
|
||||
|
||||
it('should match (terminal)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b');
|
||||
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
|
||||
],
|
||||
},
|
||||
],
|
||||
'a/b',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -316,15 +366,18 @@ describe('recognize', async () => {
|
|||
describe('aux split at the end (no right child)', async () => {
|
||||
it('should match (non-terminal)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a');
|
||||
],
|
||||
},
|
||||
],
|
||||
'a',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -334,15 +387,18 @@ describe('recognize', async () => {
|
|||
|
||||
it('should match (terminal)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', pathMatch: 'full', component: ComponentB},
|
||||
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a');
|
||||
],
|
||||
},
|
||||
],
|
||||
'a',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -352,15 +408,18 @@ describe('recognize', async () => {
|
|||
|
||||
it('should work only only primary outlet', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a/(aux:c)');
|
||||
],
|
||||
},
|
||||
],
|
||||
'a/(aux:c)',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -370,11 +429,13 @@ describe('recognize', async () => {
|
|||
|
||||
it('should work when split is at the root level', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{path: '', component: ComponentA}, {path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
'(aux:c)');
|
||||
[
|
||||
{path: '', component: ComponentA},
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
],
|
||||
'(aux:c)',
|
||||
);
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
|
||||
const children = s.root.children;
|
||||
|
|
@ -387,7 +448,8 @@ describe('recognize', async () => {
|
|||
describe('split at the end (right child)', async () => {
|
||||
it('should match (non-terminal)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
|
|
@ -396,11 +458,13 @@ describe('recognize', async () => {
|
|||
path: '',
|
||||
component: ComponentC,
|
||||
outlet: 'aux',
|
||||
children: [{path: 'e', component: ComponentE}]
|
||||
children: [{path: 'e', component: ComponentE}],
|
||||
},
|
||||
]
|
||||
}],
|
||||
'a/(d//aux:e)');
|
||||
],
|
||||
},
|
||||
],
|
||||
'a/(d//aux:e)',
|
||||
);
|
||||
checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA);
|
||||
|
||||
const c = s.root.firstChild!.children;
|
||||
|
|
@ -414,62 +478,71 @@ describe('recognize', async () => {
|
|||
describe('with outlets', async () => {
|
||||
it('should work when outlet is a child of empty path parent', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', outlet: 'b', component: ComponentB}]
|
||||
}],
|
||||
'(b:b)');
|
||||
children: [{path: 'b', outlet: 'b', component: ComponentB}],
|
||||
},
|
||||
],
|
||||
'(b:b)',
|
||||
);
|
||||
checkActivatedRoute(s.root.children[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b');
|
||||
});
|
||||
|
||||
it('should work for outlets adjacent to empty path', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{path: '', component: ComponentC}],
|
||||
},
|
||||
{path: 'b', outlet: 'b', component: ComponentB}
|
||||
],
|
||||
'(b:b)');
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{path: '', component: ComponentC}],
|
||||
},
|
||||
{path: 'b', outlet: 'b', component: ComponentB},
|
||||
],
|
||||
'(b:b)',
|
||||
);
|
||||
const [primaryChild, outletChild] = s.root.children;
|
||||
checkActivatedRoute(primaryChild, '', {}, ComponentA);
|
||||
checkActivatedRoute(outletChild, 'b', {}, ComponentB, 'b');
|
||||
checkActivatedRoute(primaryChild.children[0], '', {}, ComponentC);
|
||||
});
|
||||
|
||||
it('should work with named outlets both adjecent to and as a child of empty path',
|
||||
async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', outlet: 'b', component: ComponentB}]
|
||||
},
|
||||
{path: 'c', outlet: 'c', component: ComponentC}
|
||||
],
|
||||
'(b:b//c:c)');
|
||||
checkActivatedRoute(s.root.children[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(s.root.children[1], 'c', {}, ComponentC, 'c');
|
||||
checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b');
|
||||
});
|
||||
it('should work with named outlets both adjecent to and as a child of empty path', async () => {
|
||||
const s = await recognize(
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', outlet: 'b', component: ComponentB}],
|
||||
},
|
||||
{path: 'c', outlet: 'c', component: ComponentC},
|
||||
],
|
||||
'(b:b//c:c)',
|
||||
);
|
||||
checkActivatedRoute(s.root.children[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(s.root.children[1], 'c', {}, ComponentC, 'c');
|
||||
checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b');
|
||||
});
|
||||
|
||||
it('should work with children outlets within two levels of empty parents', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [{
|
||||
path: '',
|
||||
component: ComponentB,
|
||||
children: [{path: 'c', outlet: 'c', component: ComponentC}],
|
||||
}]
|
||||
}],
|
||||
'(c:c)');
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ComponentB,
|
||||
children: [{path: 'c', outlet: 'c', component: ComponentC}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'(c:c)',
|
||||
);
|
||||
const [compAConfig] = s.root.children;
|
||||
checkActivatedRoute(compAConfig, '', {}, ComponentA);
|
||||
expect(compAConfig.children.length).toBe(1);
|
||||
|
|
@ -482,24 +555,27 @@ describe('recognize', async () => {
|
|||
checkActivatedRoute(compCConfig, 'c', {}, ComponentC, 'c');
|
||||
});
|
||||
|
||||
it('should not persist a primary segment beyond the boundary of a named outlet match',
|
||||
async () => {
|
||||
const recognizePromise = new Recognizer(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
TestBed.inject(RouterConfigLoader), RootComponent,
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
outlet: 'a',
|
||||
children: [{path: 'b', component: ComponentB}],
|
||||
},
|
||||
],
|
||||
tree('/b'), 'emptyOnly', new DefaultUrlSerializer())
|
||||
.recognize()
|
||||
.toPromise();
|
||||
await expectAsync(recognizePromise).toBeRejected();
|
||||
});
|
||||
it('should not persist a primary segment beyond the boundary of a named outlet match', async () => {
|
||||
const recognizePromise = new Recognizer(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
TestBed.inject(RouterConfigLoader),
|
||||
RootComponent,
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
outlet: 'a',
|
||||
children: [{path: 'b', component: ComponentB}],
|
||||
},
|
||||
],
|
||||
tree('/b'),
|
||||
'emptyOnly',
|
||||
new DefaultUrlSerializer(),
|
||||
)
|
||||
.recognize()
|
||||
.toPromise();
|
||||
await expectAsync(recognizePromise).toBeRejected();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -513,13 +589,17 @@ describe('recognize', async () => {
|
|||
describe('componentless routes', async () => {
|
||||
it('should work', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'p/:id',
|
||||
children: [
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'p/11;pp=22/(a;pa=33//aux:b;pb=44)');
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'aux'},
|
||||
],
|
||||
},
|
||||
],
|
||||
'p/11;pp=22/(a;pa=33//aux:b;pb=44)',
|
||||
);
|
||||
const p = s.root.firstChild!;
|
||||
checkActivatedRoute(p, 'p/11', {id: '11', pp: '22'}, null);
|
||||
|
||||
|
|
@ -530,16 +610,25 @@ describe('recognize', async () => {
|
|||
|
||||
it('should inherit params until encounters a normal route', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'p/:id',
|
||||
children: [{
|
||||
path: 'a/:name',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB, children: [{path: 'c', component: ComponentC}]}
|
||||
]
|
||||
}]
|
||||
}],
|
||||
'p/11/a/victor/b/c');
|
||||
children: [
|
||||
{
|
||||
path: 'a/:name',
|
||||
children: [
|
||||
{
|
||||
path: 'b',
|
||||
component: ComponentB,
|
||||
children: [{path: 'c', component: ComponentC}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'p/11/a/victor/b/c',
|
||||
);
|
||||
const p = s.root.firstChild!;
|
||||
checkActivatedRoute(p, 'p/11', {id: '11'}, null);
|
||||
|
||||
|
|
@ -553,18 +642,28 @@ describe('recognize', async () => {
|
|||
checkActivatedRoute(c, 'c', {}, ComponentC);
|
||||
});
|
||||
|
||||
it('should inherit all params if paramsInheritanceStrategy is \'always\'', async () => {
|
||||
it("should inherit all params if paramsInheritanceStrategy is 'always'", async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'p/:id',
|
||||
children: [{
|
||||
path: 'a/:name',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB, children: [{path: 'c', component: ComponentC}]}
|
||||
]
|
||||
}]
|
||||
}],
|
||||
'p/11/a/victor/b/c', 'always');
|
||||
children: [
|
||||
{
|
||||
path: 'a/:name',
|
||||
children: [
|
||||
{
|
||||
path: 'b',
|
||||
component: ComponentB,
|
||||
children: [{path: 'c', component: ComponentC}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'p/11/a/victor/b/c',
|
||||
'always',
|
||||
);
|
||||
const c = s.root.firstChild!.firstChild!.firstChild!.firstChild!;
|
||||
checkActivatedRoute(c, 'c', {id: '11', name: 'victor'}, ComponentC);
|
||||
});
|
||||
|
|
@ -573,23 +672,27 @@ describe('recognize', async () => {
|
|||
describe('empty URL leftovers', async () => {
|
||||
it('should not throw when no children matching', async () => {
|
||||
const s = await recognize(
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
|
||||
'/a');
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
|
||||
'/a',
|
||||
);
|
||||
const a = s.root.firstChild;
|
||||
checkActivatedRoute(a!, 'a', {}, ComponentA);
|
||||
});
|
||||
|
||||
it('should not throw when no children matching (aux routes)', async () => {
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'/a');
|
||||
],
|
||||
},
|
||||
],
|
||||
'/a',
|
||||
);
|
||||
const a = s.root.firstChild!;
|
||||
checkActivatedRoute(a, 'a', {}, ComponentA);
|
||||
checkActivatedRoute(a.children[0], '', {}, ComponentC, 'aux');
|
||||
|
|
@ -607,19 +710,22 @@ describe('recognize', async () => {
|
|||
};
|
||||
|
||||
const s = await recognize(
|
||||
[{
|
||||
[
|
||||
{
|
||||
matcher: matcher,
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', component: ComponentB}]
|
||||
}] as any,
|
||||
'/a/1;p=99/b');
|
||||
children: [{path: 'b', component: ComponentB}],
|
||||
},
|
||||
] as any,
|
||||
'/a/1;p=99/b',
|
||||
);
|
||||
const a = s.root.firstChild!;
|
||||
checkActivatedRoute(a, 'a/1', {id: '1', p: '99'}, ComponentA);
|
||||
checkActivatedRoute(a.firstChild!, 'b', {}, ComponentB);
|
||||
});
|
||||
|
||||
it('should work with terminal route', async () => {
|
||||
const matcher = (s: any, g: any, r: any) => s.length === 0 ? ({consumed: s}) : null;
|
||||
const matcher = (s: any, g: any, r: any) => (s.length === 0 ? {consumed: s} : null);
|
||||
|
||||
const s = await recognize([{matcher, component: ComponentA}], '');
|
||||
const a = s.root.firstChild!;
|
||||
|
|
@ -627,10 +733,12 @@ describe('recognize', async () => {
|
|||
});
|
||||
|
||||
it('should work with child terminal route', async () => {
|
||||
const matcher = (s: any, g: any, r: any) => s.length === 0 ? ({consumed: s}) : null;
|
||||
const matcher = (s: any, g: any, r: any) => (s.length === 0 ? {consumed: s} : null);
|
||||
|
||||
const s = await recognize(
|
||||
[{path: 'a', component: ComponentA, children: [{matcher, component: ComponentB}]}], 'a');
|
||||
[{path: 'a', component: ComponentA, children: [{matcher, component: ComponentB}]}],
|
||||
'a',
|
||||
);
|
||||
const a = s.root.firstChild!;
|
||||
checkActivatedRoute(a, 'a', {}, ComponentA);
|
||||
});
|
||||
|
|
@ -668,7 +776,7 @@ describe('recognize', async () => {
|
|||
it('should run canMatch guards on wildcard routes', async () => {
|
||||
const config = [
|
||||
{path: '**', component: ComponentA, data: {id: 'a'}, canMatch: [() => false]},
|
||||
{path: '**', component: ComponentB, data: {id: 'b'}}
|
||||
{path: '**', component: ComponentB, data: {id: 'b'}},
|
||||
];
|
||||
const s = await recognize(config, 'a');
|
||||
expect(s.root.firstChild!.data['id']).toEqual('b');
|
||||
|
|
@ -677,24 +785,36 @@ describe('recognize', async () => {
|
|||
});
|
||||
|
||||
async function recognize(
|
||||
config: Routes, url: string,
|
||||
paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly'): Promise<RouterStateSnapshot> {
|
||||
config: Routes,
|
||||
url: string,
|
||||
paramsInheritanceStrategy: 'emptyOnly' | 'always' = 'emptyOnly',
|
||||
): Promise<RouterStateSnapshot> {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
const result = await new Recognizer(
|
||||
TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader),
|
||||
RootComponent, config, tree(url), paramsInheritanceStrategy, serializer)
|
||||
.recognize()
|
||||
.toPromise();
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
TestBed.inject(RouterConfigLoader),
|
||||
RootComponent,
|
||||
config,
|
||||
tree(url),
|
||||
paramsInheritanceStrategy,
|
||||
serializer,
|
||||
)
|
||||
.recognize()
|
||||
.toPromise();
|
||||
return result!.state;
|
||||
}
|
||||
|
||||
function checkActivatedRoute(
|
||||
actual: ActivatedRouteSnapshot, url: string, params: Params, cmp: Function|null,
|
||||
outlet: string = PRIMARY_OUTLET): void {
|
||||
actual: ActivatedRouteSnapshot,
|
||||
url: string,
|
||||
params: Params,
|
||||
cmp: Function | null,
|
||||
outlet: string = PRIMARY_OUTLET,
|
||||
): void {
|
||||
if (actual === null) {
|
||||
expect(actual).not.toBeNull();
|
||||
} else {
|
||||
expect(actual.url.map(s => s.path).join('/')).toEqual(url);
|
||||
expect(actual.url.map((s) => s.path).join('/')).toEqual(url);
|
||||
expect(actual.params).toEqual(params);
|
||||
expect(actual.component).toBe(cmp);
|
||||
expect(actual.outlet).toEqual(outlet);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,27 @@
|
|||
|
||||
import {CommonModule, HashLocationStrategy, Location, LocationStrategy} from '@angular/common';
|
||||
import {provideLocationMocks, SpyLocation} from '@angular/common/testing';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Injectable,
|
||||
NgModule,
|
||||
TemplateRef,
|
||||
Type,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {ChildrenOutletContexts, DefaultUrlSerializer, Router, RouterModule, RouterOutlet, UrlSerializer, UrlTree} from '@angular/router';
|
||||
import {
|
||||
ChildrenOutletContexts,
|
||||
DefaultUrlSerializer,
|
||||
Router,
|
||||
RouterModule,
|
||||
RouterOutlet,
|
||||
UrlSerializer,
|
||||
UrlTree,
|
||||
} from '@angular/router';
|
||||
import {of} from 'rxjs';
|
||||
import {delay, mapTo} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -19,97 +37,98 @@ import {provideRouter} from '../src/provide_router';
|
|||
describe('Integration', () => {
|
||||
describe('routerLinkActive', () => {
|
||||
it('should update when the associated routerLinks change - #18469', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `
|
||||
@Component({
|
||||
template: `
|
||||
<a id="first-link" [routerLink]="[firstLink]" routerLinkActive="active">{{firstLink}}</a>
|
||||
<div id="second-link" routerLinkActive="active">
|
||||
<a [routerLink]="[secondLink]">{{secondLink}}</a>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class LinkComponent {
|
||||
firstLink = 'link-a';
|
||||
secondLink = 'link-b';
|
||||
})
|
||||
class LinkComponent {
|
||||
firstLink = 'link-a';
|
||||
secondLink = 'link-b';
|
||||
|
||||
changeLinks(): void {
|
||||
const temp = this.secondLink;
|
||||
this.secondLink = this.firstLink;
|
||||
this.firstLink = temp;
|
||||
}
|
||||
}
|
||||
changeLinks(): void {
|
||||
const temp = this.secondLink;
|
||||
this.secondLink = this.firstLink;
|
||||
this.firstLink = temp;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot(
|
||||
[{path: 'link-a', component: SimpleCmp}, {path: 'link-b', component: SimpleCmp}])],
|
||||
declarations: [LinkComponent, SimpleCmp]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: 'link-a', component: SimpleCmp},
|
||||
{path: 'link-b', component: SimpleCmp},
|
||||
]),
|
||||
],
|
||||
declarations: [LinkComponent, SimpleCmp],
|
||||
});
|
||||
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, LinkComponent);
|
||||
const firstLink = fixture.debugElement.query(p => p.nativeElement.id === 'first-link');
|
||||
const secondLink = fixture.debugElement.query(p => p.nativeElement.id === 'second-link');
|
||||
router.navigateByUrl('/link-a');
|
||||
advance(fixture);
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, LinkComponent);
|
||||
const firstLink = fixture.debugElement.query((p) => p.nativeElement.id === 'first-link');
|
||||
const secondLink = fixture.debugElement.query((p) => p.nativeElement.id === 'second-link');
|
||||
router.navigateByUrl('/link-a');
|
||||
advance(fixture);
|
||||
|
||||
expect(firstLink.nativeElement.classList).toContain('active');
|
||||
expect(secondLink.nativeElement.classList).not.toContain('active');
|
||||
expect(firstLink.nativeElement.classList).toContain('active');
|
||||
expect(secondLink.nativeElement.classList).not.toContain('active');
|
||||
|
||||
fixture.componentInstance.changeLinks();
|
||||
fixture.detectChanges();
|
||||
advance(fixture);
|
||||
fixture.componentInstance.changeLinks();
|
||||
fixture.detectChanges();
|
||||
advance(fixture);
|
||||
|
||||
expect(firstLink.nativeElement.classList).not.toContain('active');
|
||||
expect(secondLink.nativeElement.classList).toContain('active');
|
||||
}));
|
||||
expect(firstLink.nativeElement.classList).not.toContain('active');
|
||||
expect(secondLink.nativeElement.classList).toContain('active');
|
||||
}));
|
||||
|
||||
it('should not cause infinite loops in the change detection - #15825', fakeAsync(() => {
|
||||
@Component({selector: 'simple', template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
@Component({selector: 'simple', template: 'simple'})
|
||||
class SimpleCmp {}
|
||||
|
||||
@Component({
|
||||
selector: 'some-root',
|
||||
template: `
|
||||
@Component({
|
||||
selector: 'some-root',
|
||||
template: `
|
||||
<div *ngIf="show">
|
||||
<ng-container *ngTemplateOutlet="tpl"></ng-container>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
<ng-template #tpl>
|
||||
<a routerLink="/simple" routerLinkActive="active"></a>
|
||||
</ng-template>`
|
||||
})
|
||||
class MyCmp {
|
||||
show: boolean = false;
|
||||
}
|
||||
</ng-template>`,
|
||||
})
|
||||
class MyCmp {
|
||||
show: boolean = false;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule.forRoot([])],
|
||||
declarations: [MyCmp, SimpleCmp],
|
||||
})
|
||||
class MyModule {
|
||||
}
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule.forRoot([])],
|
||||
declarations: [MyCmp, SimpleCmp],
|
||||
})
|
||||
class MyModule {}
|
||||
|
||||
TestBed.configureTestingModule({imports: [MyModule]});
|
||||
TestBed.configureTestingModule({imports: [MyModule]});
|
||||
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, MyCmp);
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, MyCmp);
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
const instance = fixture.componentInstance;
|
||||
instance.show = true;
|
||||
expect(() => advance(fixture)).not.toThrow();
|
||||
}));
|
||||
const instance = fixture.componentInstance;
|
||||
instance.show = true;
|
||||
expect(() => advance(fixture)).not.toThrow();
|
||||
}));
|
||||
|
||||
it('should set isActive right after looking at its children -- #18983', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `
|
||||
@Component({
|
||||
template: `
|
||||
<div #rla="routerLinkActive" routerLinkActive>
|
||||
isActive: {{rla.isActive}}
|
||||
|
||||
|
|
@ -119,162 +138,155 @@ describe('Integration', () => {
|
|||
|
||||
<ng-container #container></ng-container>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
class ComponentWithRouterLink {
|
||||
@ViewChild(TemplateRef, {static: true}) templateRef?: TemplateRef<unknown>;
|
||||
@ViewChild('container', {read: ViewContainerRef, static: true})
|
||||
container?: ViewContainerRef;
|
||||
`,
|
||||
})
|
||||
class ComponentWithRouterLink {
|
||||
@ViewChild(TemplateRef, {static: true}) templateRef?: TemplateRef<unknown>;
|
||||
@ViewChild('container', {read: ViewContainerRef, static: true})
|
||||
container?: ViewContainerRef;
|
||||
|
||||
addLink() {
|
||||
if (this.templateRef) {
|
||||
this.container?.createEmbeddedView(this.templateRef, {$implicit: '/simple'});
|
||||
}
|
||||
}
|
||||
addLink() {
|
||||
if (this.templateRef) {
|
||||
this.container?.createEmbeddedView(this.templateRef, {$implicit: '/simple'});
|
||||
}
|
||||
}
|
||||
|
||||
removeLink() {
|
||||
this.container?.clear();
|
||||
}
|
||||
}
|
||||
removeLink() {
|
||||
this.container?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])],
|
||||
declarations: [ComponentWithRouterLink, SimpleCmp]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])],
|
||||
declarations: [ComponentWithRouterLink, SimpleCmp],
|
||||
});
|
||||
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, ComponentWithRouterLink);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, ComponentWithRouterLink);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
fixture.componentInstance.addLink();
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.addLink();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.removeLink();
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
fixture.componentInstance.removeLink();
|
||||
advance(fixture);
|
||||
advance(fixture);
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toContain('isActive: false');
|
||||
}));
|
||||
expect(fixture.nativeElement.innerHTML).toContain('isActive: false');
|
||||
}));
|
||||
|
||||
it('should set isActive with OnPush change detection - #19934', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `
|
||||
@Component({
|
||||
template: `
|
||||
<div routerLink="/simple" #rla="routerLinkActive" routerLinkActive>
|
||||
isActive: {{rla.isActive}}
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
class OnPushComponent {
|
||||
}
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class OnPushComponent {}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])],
|
||||
declarations: [OnPushComponent, SimpleCmp]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])],
|
||||
declarations: [OnPushComponent, SimpleCmp],
|
||||
});
|
||||
|
||||
const router: Router = TestBed.get(Router);
|
||||
const fixture = createRoot(router, OnPushComponent);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
const router: Router = TestBed.get(Router);
|
||||
const fixture = createRoot(router, OnPushComponent);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toContain('isActive: true');
|
||||
}));
|
||||
expect(fixture.nativeElement.innerHTML).toContain('isActive: true');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not reactivate a deactivated outlet when destroyed and recreated - #41379',
|
||||
fakeAsync(() => {
|
||||
@Component({template: 'simple'})
|
||||
class SimpleComponent {
|
||||
}
|
||||
it('should not reactivate a deactivated outlet when destroyed and recreated - #41379', fakeAsync(() => {
|
||||
@Component({template: 'simple'})
|
||||
class SimpleComponent {}
|
||||
|
||||
@Component({template: ` <router-outlet *ngIf="outletVisible" name="aux"></router-outlet> `})
|
||||
class AppComponent {
|
||||
outletVisible = true;
|
||||
}
|
||||
@Component({template: ` <router-outlet *ngIf="outletVisible" name="aux"></router-outlet> `})
|
||||
class AppComponent {
|
||||
outletVisible = true;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports:
|
||||
[RouterModule.forRoot([{path: ':id', component: SimpleComponent, outlet: 'aux'}])],
|
||||
declarations: [SimpleComponent, AppComponent],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: ':id', component: SimpleComponent, outlet: 'aux'}])],
|
||||
declarations: [SimpleComponent, AppComponent],
|
||||
});
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, AppComponent);
|
||||
const componentCdr = fixture.componentRef.injector.get<ChangeDetectorRef>(ChangeDetectorRef);
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, AppComponent);
|
||||
const componentCdr = fixture.componentRef.injector.get<ChangeDetectorRef>(ChangeDetectorRef);
|
||||
|
||||
router.navigate([{outlets: {aux: ['1234']}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
router.navigate([{outlets: {aux: ['1234']}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
|
||||
router.navigate([{outlets: {aux: null}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
router.navigate([{outlets: {aux: null}}]);
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
|
||||
fixture.componentInstance.outletVisible = false;
|
||||
componentCdr.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('router-outlet');
|
||||
fixture.componentInstance.outletVisible = false;
|
||||
componentCdr.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('router-outlet');
|
||||
|
||||
fixture.componentInstance.outletVisible = true;
|
||||
componentCdr.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).toContain('router-outlet');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
}));
|
||||
fixture.componentInstance.outletVisible = true;
|
||||
componentCdr.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).toContain('router-outlet');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
}));
|
||||
|
||||
describe('useHash', () => {
|
||||
it('should restore hash to match current route - #28561', fakeAsync(() => {
|
||||
@Component({selector: 'root-cmp', template: `<router-outlet></router-outlet>`})
|
||||
class RootCmp {
|
||||
}
|
||||
@Component({selector: 'root-cmp', template: `<router-outlet></router-outlet>`})
|
||||
class RootCmp {}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
@Component({template: 'one'})
|
||||
class OneCmp {
|
||||
}
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {}
|
||||
@Component({template: 'one'})
|
||||
class OneCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'one', component: OneCmp, canActivate: ['returnRootUrlTree']}
|
||||
])],
|
||||
declarations: [SimpleCmp, RootCmp, OneCmp],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
{
|
||||
provide: 'returnRootUrlTree',
|
||||
useFactory: (router: Router) => () => {
|
||||
return router.parseUrl('/');
|
||||
},
|
||||
deps: [Router]
|
||||
},
|
||||
],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'one', component: OneCmp, canActivate: ['returnRootUrlTree']},
|
||||
]),
|
||||
],
|
||||
declarations: [SimpleCmp, RootCmp, OneCmp],
|
||||
providers: [
|
||||
provideLocationMocks(),
|
||||
{
|
||||
provide: 'returnRootUrlTree',
|
||||
useFactory: (router: Router) => () => {
|
||||
return router.parseUrl('/');
|
||||
},
|
||||
deps: [Router],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location) as SpyLocation;
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location) as SpyLocation;
|
||||
|
||||
router.navigateByUrl('/');
|
||||
// Will setup location change listeners
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
router.navigateByUrl('/');
|
||||
// Will setup location change listeners
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
location.simulateHashChange('/one');
|
||||
advance(fixture);
|
||||
location.simulateHashChange('/one');
|
||||
advance(fixture);
|
||||
|
||||
expect(location.path()).toEqual('/');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
expect(location.path()).toEqual('/');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('duplicate navigation handling (#43447, #43446)', () => {
|
||||
|
|
@ -290,27 +302,22 @@ describe('Integration', () => {
|
|||
}
|
||||
}
|
||||
@Component({selector: 'root-cmp', template: `<router-outlet></router-outlet>`})
|
||||
class RootCmp {
|
||||
}
|
||||
class RootCmp {}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
class SimpleCmp {}
|
||||
@Component({template: 'one'})
|
||||
class OneCmp {
|
||||
}
|
||||
class OneCmp {}
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SimpleCmp, RootCmp, OneCmp],
|
||||
imports: [RouterOutlet],
|
||||
providers: [
|
||||
DelayedResolve,
|
||||
provideLocationMocks(),
|
||||
provideRouter(
|
||||
[
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'one', component: OneCmp, resolve: {x: DelayedResolve}}
|
||||
],
|
||||
),
|
||||
provideRouter([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'one', component: OneCmp, resolve: {x: DelayedResolve}},
|
||||
]),
|
||||
{provide: LocationStrategy, useClass: HashLocationStrategy},
|
||||
],
|
||||
});
|
||||
|
|
@ -324,27 +331,26 @@ describe('Integration', () => {
|
|||
}));
|
||||
|
||||
it('duplicate navigation to same url', fakeAsync(() => {
|
||||
location.go('/one');
|
||||
tick(100);
|
||||
location.go('/one');
|
||||
tick(1000);
|
||||
advance(fixture);
|
||||
location.go('/one');
|
||||
tick(100);
|
||||
location.go('/one');
|
||||
tick(1000);
|
||||
advance(fixture);
|
||||
|
||||
expect(location.path()).toEqual('/one');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
expect(location.path()).toEqual('/one');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
|
||||
it('works with a duplicate popstate/hashchange navigation (as seen in firefox)',
|
||||
fakeAsync(() => {
|
||||
(location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'popstate'});
|
||||
tick(1);
|
||||
(location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'hashchange'});
|
||||
tick(1000);
|
||||
advance(fixture);
|
||||
it('works with a duplicate popstate/hashchange navigation (as seen in firefox)', fakeAsync(() => {
|
||||
(location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'popstate'});
|
||||
tick(1);
|
||||
(location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'hashchange'});
|
||||
tick(1000);
|
||||
advance(fixture);
|
||||
|
||||
expect(router.routerState.toString()).toContain(`url:'one'`);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
expect(router.routerState.toString()).toContain(`url:'one'`);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('one');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not unregister outlet if a different one already exists #36711, 32453', async () => {
|
||||
|
|
@ -360,12 +366,11 @@ describe('Integration', () => {
|
|||
}
|
||||
|
||||
@Component({template: ''})
|
||||
class EmptyCmp {
|
||||
}
|
||||
class EmptyCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterModule.forRoot([{path: '**', component: EmptyCmp}])],
|
||||
declarations: [TestCmp, EmptyCmp]
|
||||
declarations: [TestCmp, EmptyCmp],
|
||||
});
|
||||
const fixture = TestBed.createComponent(TestCmp);
|
||||
const contexts = TestBed.inject(ChildrenOutletContexts);
|
||||
|
|
@ -403,7 +408,7 @@ describe('Integration', () => {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([]), {provide: UrlSerializer, useValue: new CustomSerializer()}]
|
||||
providers: [provideRouter([]), {provide: UrlSerializer, useValue: new CustomSerializer()}],
|
||||
});
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
|
|
|
|||
|
|
@ -36,11 +36,16 @@ describe('Router', () => {
|
|||
|
||||
it('should copy config to avoid mutations of user-provided objects', () => {
|
||||
const r: Router = TestBed.inject(Router);
|
||||
const configs: Routes = [{
|
||||
path: 'a',
|
||||
component: TestComponent,
|
||||
children: [{path: 'b', component: TestComponent}, {path: 'c', component: TestComponent}]
|
||||
}];
|
||||
const configs: Routes = [
|
||||
{
|
||||
path: 'a',
|
||||
component: TestComponent,
|
||||
children: [
|
||||
{path: 'b', component: TestComponent},
|
||||
{path: 'c', component: TestComponent},
|
||||
],
|
||||
},
|
||||
];
|
||||
const children = configs[0].children!;
|
||||
|
||||
r.resetConfig(configs);
|
||||
|
|
@ -86,19 +91,19 @@ describe('Router', () => {
|
|||
});
|
||||
|
||||
it('should be idempotent', inject([Router, Location], (r: Router, location: Location) => {
|
||||
r.setUpLocationChangeListener();
|
||||
const a = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
r.setUpLocationChangeListener();
|
||||
const b = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
r.setUpLocationChangeListener();
|
||||
const a = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
r.setUpLocationChangeListener();
|
||||
const b = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
|
||||
expect(a).toBe(b);
|
||||
expect(a).toBe(b);
|
||||
|
||||
r.dispose();
|
||||
r.setUpLocationChangeListener();
|
||||
const c = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
r.dispose();
|
||||
r.setUpLocationChangeListener();
|
||||
const c = (<any>r).nonRouterCurrentEntryChangeSubscription;
|
||||
|
||||
expect(c).not.toBe(b);
|
||||
}));
|
||||
expect(c).not.toBe(b);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('PreActivation', () => {
|
||||
|
|
@ -127,20 +132,32 @@ describe('Router', () => {
|
|||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule],
|
||||
providers: [
|
||||
Logger, provideTokenLogger(CA_CHILD), provideTokenLogger(CA_CHILD_FALSE, false),
|
||||
Logger,
|
||||
provideTokenLogger(CA_CHILD),
|
||||
provideTokenLogger(CA_CHILD_FALSE, false),
|
||||
provideTokenLogger(CA_CHILD_REDIRECT, serializer.parse('/canActivate_child_redirect')),
|
||||
provideTokenLogger(CAC_CHILD), provideTokenLogger(CAC_CHILD_FALSE, false),
|
||||
provideTokenLogger(CAC_CHILD),
|
||||
provideTokenLogger(CAC_CHILD_FALSE, false),
|
||||
provideTokenLogger(
|
||||
CAC_CHILD_REDIRECT, serializer.parse('/canActivateChild_child_redirect')),
|
||||
provideTokenLogger(CA_GRANDCHILD), provideTokenLogger(CA_GRANDCHILD_FALSE, false),
|
||||
CAC_CHILD_REDIRECT,
|
||||
serializer.parse('/canActivateChild_child_redirect'),
|
||||
),
|
||||
provideTokenLogger(CA_GRANDCHILD),
|
||||
provideTokenLogger(CA_GRANDCHILD_FALSE, false),
|
||||
provideTokenLogger(
|
||||
CA_GRANDCHILD_REDIRECT, serializer.parse('/canActivate_grandchild_redirect')),
|
||||
provideTokenLogger(CDA_CHILD), provideTokenLogger(CDA_CHILD_FALSE, false),
|
||||
CA_GRANDCHILD_REDIRECT,
|
||||
serializer.parse('/canActivate_grandchild_redirect'),
|
||||
),
|
||||
provideTokenLogger(CDA_CHILD),
|
||||
provideTokenLogger(CDA_CHILD_FALSE, false),
|
||||
provideTokenLogger(CDA_CHILD_REDIRECT, serializer.parse('/canDeactivate_child_redirect')),
|
||||
provideTokenLogger(CDA_GRANDCHILD), provideTokenLogger(CDA_GRANDCHILD_FALSE, false),
|
||||
provideTokenLogger(CDA_GRANDCHILD),
|
||||
provideTokenLogger(CDA_GRANDCHILD_FALSE, false),
|
||||
provideTokenLogger(
|
||||
CDA_GRANDCHILD_REDIRECT, serializer.parse('/canDeactivate_grandchild_redirect'))
|
||||
]
|
||||
CDA_GRANDCHILD_REDIRECT,
|
||||
serializer.parse('/canDeactivate_grandchild_redirect'),
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -158,25 +175,32 @@ describe('Router', () => {
|
|||
* child
|
||||
*/
|
||||
let result = false;
|
||||
const childSnapshot =
|
||||
createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {path: 'child'},
|
||||
});
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]),
|
||||
);
|
||||
// Since we only test the guards, we don't need to provide a full navigation
|
||||
// transition object with all properties set.
|
||||
const testTransition = {
|
||||
guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts())
|
||||
guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition;
|
||||
|
||||
of(testTransition)
|
||||
.pipe(checkGuardsOperator(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
(evt) => {
|
||||
events.push(evt);
|
||||
}))
|
||||
.subscribe((x) => result = !!x.guardsResult, (e) => {
|
||||
.pipe(
|
||||
checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => {
|
||||
events.push(evt);
|
||||
}),
|
||||
)
|
||||
.subscribe(
|
||||
(x) => (result = !!x.guardsResult),
|
||||
(e) => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(events.length).toEqual(2);
|
||||
|
|
@ -195,33 +219,44 @@ describe('Router', () => {
|
|||
* great grandchild
|
||||
*/
|
||||
let result = false;
|
||||
const childSnapshot =
|
||||
createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {path: 'grandchild'}});
|
||||
const greatGrandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'great-grandchild', routeConfig: {path: 'great-grandchild'}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {path: 'child'},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {path: 'grandchild'},
|
||||
});
|
||||
const greatGrandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'great-grandchild',
|
||||
routeConfig: {path: 'great-grandchild'},
|
||||
});
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [
|
||||
new TreeNode(grandchildSnapshot, [new TreeNode(greatGrandchildSnapshot, [])])
|
||||
])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [
|
||||
new TreeNode(grandchildSnapshot, [new TreeNode(greatGrandchildSnapshot, [])]),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
// Since we only test the guards, we don't need to provide a full navigation
|
||||
// transition object with all properties set.
|
||||
const testTransition = {
|
||||
guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts())
|
||||
guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition;
|
||||
|
||||
of(testTransition)
|
||||
.pipe(checkGuardsOperator(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
(evt) => {
|
||||
events.push(evt);
|
||||
}))
|
||||
.subscribe((x) => result = !!x.guardsResult, (e) => {
|
||||
.pipe(
|
||||
checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => {
|
||||
events.push(evt);
|
||||
}),
|
||||
)
|
||||
.subscribe(
|
||||
(x) => (result = !!x.guardsResult),
|
||||
(e) => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(events.length).toEqual(6);
|
||||
|
|
@ -240,31 +275,42 @@ describe('Router', () => {
|
|||
* grandchild
|
||||
*/
|
||||
let result = false;
|
||||
const childSnapshot =
|
||||
createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {path: 'grandchild'}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {path: 'child'},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {path: 'grandchild'},
|
||||
});
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]),
|
||||
);
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
// Since we only test the guards, we don't need to provide a full navigation
|
||||
// transition object with all properties set.
|
||||
const testTransition = {
|
||||
guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts())
|
||||
guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition;
|
||||
|
||||
of(testTransition)
|
||||
.pipe(checkGuardsOperator(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
(evt) => {
|
||||
events.push(evt);
|
||||
}))
|
||||
.subscribe((x) => result = !!x.guardsResult, (e) => {
|
||||
.pipe(
|
||||
checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => {
|
||||
events.push(evt);
|
||||
}),
|
||||
)
|
||||
.subscribe(
|
||||
(x) => (result = !!x.guardsResult),
|
||||
(e) => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(events.length).toEqual(2);
|
||||
|
|
@ -285,42 +331,58 @@ describe('Router', () => {
|
|||
* great-greatgrandchild
|
||||
*/
|
||||
let result = false;
|
||||
const childSnapshot =
|
||||
createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {path: 'grandchild'}});
|
||||
const greatGrandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'greatgrandchild', routeConfig: {path: 'greatgrandchild'}});
|
||||
const greatGreatGrandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'great-greatgrandchild', routeConfig: {path: 'great-greatgrandchild'}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {path: 'child'},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {path: 'grandchild'},
|
||||
});
|
||||
const greatGrandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'greatgrandchild',
|
||||
routeConfig: {path: 'greatgrandchild'},
|
||||
});
|
||||
const greatGreatGrandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'great-greatgrandchild',
|
||||
routeConfig: {path: 'great-greatgrandchild'},
|
||||
});
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root,
|
||||
[new TreeNode(
|
||||
childSnapshot, [new TreeNode(grandchildSnapshot, [
|
||||
new TreeNode(
|
||||
greatGrandchildSnapshot, [new TreeNode(greatGreatGrandchildSnapshot, [])])
|
||||
])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [
|
||||
new TreeNode(grandchildSnapshot, [
|
||||
new TreeNode(greatGrandchildSnapshot, [
|
||||
new TreeNode(greatGreatGrandchildSnapshot, []),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
// Since we only test the guards, we don't need to provide a full navigation
|
||||
// transition object with all properties set.
|
||||
const testTransition = {
|
||||
guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts())
|
||||
guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition;
|
||||
|
||||
of(testTransition)
|
||||
.pipe(checkGuardsOperator(
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
(evt) => {
|
||||
events.push(evt);
|
||||
}))
|
||||
.subscribe((x) => result = !!x.guardsResult, (e) => {
|
||||
.pipe(
|
||||
checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => {
|
||||
events.push(evt);
|
||||
}),
|
||||
)
|
||||
.subscribe(
|
||||
(x) => (result = !!x.guardsResult),
|
||||
(e) => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(events.length).toEqual(4);
|
||||
|
|
@ -344,18 +406,21 @@ describe('Router', () => {
|
|||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {
|
||||
|
||||
canActivate: [CA_CHILD],
|
||||
canActivateChild: [CAC_CHILD]
|
||||
}
|
||||
canActivateChild: [CAC_CHILD],
|
||||
},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(result).toBe(true);
|
||||
|
|
@ -374,15 +439,19 @@ describe('Router', () => {
|
|||
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD_FALSE], canActivateChild: [CAC_CHILD]}
|
||||
routeConfig: {canActivate: [CA_CHILD_FALSE], canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(result).toBe(false);
|
||||
|
|
@ -401,15 +470,19 @@ describe('Router', () => {
|
|||
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD_FALSE]}
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD_FALSE]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(result).toBe(false);
|
||||
|
|
@ -426,24 +499,32 @@ describe('Router', () => {
|
|||
* grandchild (CA)
|
||||
*/
|
||||
|
||||
const prevSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD]}});
|
||||
const prevSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'prev',
|
||||
routeConfig: {canDeactivate: [CDA_CHILD]},
|
||||
});
|
||||
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]));
|
||||
'prev',
|
||||
new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]),
|
||||
);
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(logger.logs).toEqual([CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD]);
|
||||
|
|
@ -459,21 +540,29 @@ describe('Router', () => {
|
|||
* grandchild (CA)
|
||||
*/
|
||||
|
||||
const prevSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD_FALSE]}});
|
||||
const prevSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'prev',
|
||||
routeConfig: {canDeactivate: [CDA_CHILD_FALSE]},
|
||||
});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]));
|
||||
'prev',
|
||||
new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]),
|
||||
);
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(result).toBe(false);
|
||||
|
|
@ -489,31 +578,45 @@ describe('Router', () => {
|
|||
* prevGrandchild(CDA) grandchild (CA)
|
||||
*/
|
||||
|
||||
const prevChildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'prev_child', routeConfig: {canDeactivate: [CDA_CHILD]}});
|
||||
const prevGrandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'prev_grandchild', routeConfig: {canDeactivate: [CDA_GRANDCHILD]}});
|
||||
const prevChildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'prev_child',
|
||||
routeConfig: {canDeactivate: [CDA_CHILD]},
|
||||
});
|
||||
const prevGrandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'prev_grandchild',
|
||||
routeConfig: {canDeactivate: [CDA_GRANDCHILD]},
|
||||
});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'prev', new TreeNode(empty.root, [
|
||||
new TreeNode(prevChildSnapshot, [new TreeNode(prevGrandchildSnapshot, [])])
|
||||
]));
|
||||
'prev',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(prevChildSnapshot, [new TreeNode(prevGrandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url',
|
||||
new TreeNode(
|
||||
empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(logger.logs).toEqual([
|
||||
CDA_GRANDCHILD, CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD
|
||||
CDA_GRANDCHILD,
|
||||
CDA_CHILD,
|
||||
CA_CHILD,
|
||||
CAC_CHILD,
|
||||
CA_GRANDCHILD,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -535,13 +638,14 @@ describe('Router', () => {
|
|||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {
|
||||
|
||||
canActivate: [CA_CHILD_REDIRECT]
|
||||
}
|
||||
canActivate: [CA_CHILD_REDIRECT],
|
||||
},
|
||||
});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(serializer.serialize(result as UrlTree)).toBe('/' + CA_CHILD_REDIRECT);
|
||||
|
|
@ -558,15 +662,21 @@ describe('Router', () => {
|
|||
* grandchild (CA)
|
||||
*/
|
||||
|
||||
const childSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'child', routeConfig: {canActivateChild: [CAC_CHILD_REDIRECT]}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivateChild: [CAC_CHILD_REDIRECT]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])
|
||||
]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(serializer.serialize(result as UrlTree)).toBe('/' + CAC_CHILD_REDIRECT);
|
||||
|
|
@ -583,15 +693,21 @@ describe('Router', () => {
|
|||
* grandchild (CA: redirect)
|
||||
*/
|
||||
|
||||
const childSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'child', routeConfig: {canActivateChild: [CAC_CHILD]}});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD_REDIRECT]}});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD_REDIRECT]},
|
||||
});
|
||||
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])
|
||||
]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(serializer.serialize(result as UrlTree)).toBe('/' + CA_GRANDCHILD_REDIRECT);
|
||||
|
|
@ -608,21 +724,29 @@ describe('Router', () => {
|
|||
* grandchild (CA)
|
||||
*/
|
||||
|
||||
const prevSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD_REDIRECT]}});
|
||||
const prevSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'prev',
|
||||
routeConfig: {canDeactivate: [CDA_CHILD_REDIRECT]},
|
||||
});
|
||||
const childSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'child',
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}
|
||||
routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot({
|
||||
component: 'grandchild',
|
||||
routeConfig: {canActivate: [CA_GRANDCHILD]},
|
||||
});
|
||||
const grandchildSnapshot = createActivatedRouteSnapshot(
|
||||
{component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}});
|
||||
|
||||
const currentState = new (RouterStateSnapshot as any)(
|
||||
'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]));
|
||||
'prev',
|
||||
new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]),
|
||||
);
|
||||
const futureState = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])
|
||||
]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [
|
||||
new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]),
|
||||
]),
|
||||
);
|
||||
|
||||
checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => {
|
||||
expect(serializer.serialize(result as UrlTree)).toBe('/' + CDA_CHILD_REDIRECT);
|
||||
|
|
@ -642,7 +766,9 @@ describe('Router', () => {
|
|||
const r = {data: () => 'resolver_value'};
|
||||
const n = createActivatedRouteSnapshot({component: 'a', resolve: r});
|
||||
const s = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(n, [])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(n, [])]),
|
||||
);
|
||||
|
||||
checkResolveData(s, empty, TestBed.inject(EnvironmentInjector), () => {
|
||||
expect(s.root.firstChild!.data).toEqual({data: 'resolver_value'});
|
||||
|
|
@ -664,7 +790,9 @@ describe('Router', () => {
|
|||
const child = createActivatedRouteSnapshot({component: 'b', resolve: childResolve});
|
||||
|
||||
const s = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])]),
|
||||
);
|
||||
|
||||
checkResolveData(s, empty, TestBed.inject(EnvironmentInjector), () => {
|
||||
expect(s.root.firstChild!.firstChild!.data).toEqual({data: 'resolver_value'});
|
||||
|
|
@ -684,13 +812,17 @@ describe('Router', () => {
|
|||
|
||||
const n1 = createActivatedRouteSnapshot({component: 'a', resolve: r1});
|
||||
const s1 = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(n1, [])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(n1, [])]),
|
||||
);
|
||||
checkResolveData(s1, empty, TestBed.inject(EnvironmentInjector), () => {});
|
||||
|
||||
const n21 = createActivatedRouteSnapshot({component: 'a', resolve: r1});
|
||||
const n22 = createActivatedRouteSnapshot({component: 'b', resolve: r2});
|
||||
const s2 = new (RouterStateSnapshot as any)(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])]));
|
||||
'url',
|
||||
new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])]),
|
||||
);
|
||||
checkResolveData(s2, s1, TestBed.inject(EnvironmentInjector), () => {
|
||||
expect(s2.root.firstChild!.data).toEqual({data: 'resolver1_value'});
|
||||
expect(s2.root.firstChild!.firstChild!.data).toEqual({data: 'resolver2_value'});
|
||||
|
|
@ -701,33 +833,41 @@ describe('Router', () => {
|
|||
});
|
||||
|
||||
function checkResolveData(
|
||||
future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: EnvironmentInjector,
|
||||
check: any): void {
|
||||
future: RouterStateSnapshot,
|
||||
curr: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
check: any,
|
||||
): void {
|
||||
// Since we only test the guards and their resolve data function, we don't need to provide
|
||||
// a full navigation transition object with all properties set.
|
||||
of({guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts())} as
|
||||
NavigationTransition)
|
||||
.pipe(resolveDataOperator('emptyOnly', injector))
|
||||
.subscribe(check, (e) => {
|
||||
throw e;
|
||||
});
|
||||
of({
|
||||
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition)
|
||||
.pipe(resolveDataOperator('emptyOnly', injector))
|
||||
.subscribe(check, (e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function checkGuards(
|
||||
future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: EnvironmentInjector,
|
||||
check: (result: boolean|UrlTree) => void): void {
|
||||
future: RouterStateSnapshot,
|
||||
curr: RouterStateSnapshot,
|
||||
injector: EnvironmentInjector,
|
||||
check: (result: boolean | UrlTree) => void,
|
||||
): void {
|
||||
// Since we only test the guards, we don't need to provide a full navigation
|
||||
// transition object with all properties set.
|
||||
of({guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts())} as
|
||||
NavigationTransition)
|
||||
.pipe(checkGuardsOperator(injector))
|
||||
.subscribe({
|
||||
next(t) {
|
||||
if (t.guardsResult === null) throw new Error('Guard result expected');
|
||||
return check(t.guardsResult);
|
||||
},
|
||||
error(e) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
of({
|
||||
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts()),
|
||||
} as NavigationTransition)
|
||||
.pipe(checkGuardsOperator(injector))
|
||||
.subscribe({
|
||||
next(t) {
|
||||
if (t.guardsResult === null) throw new Error('Guard result expected');
|
||||
return check(t.guardsResult);
|
||||
},
|
||||
error(e) {
|
||||
throw e;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ describe('RouterLink', () => {
|
|||
it('does not modify tabindex if already set on non-anchor element', () => {
|
||||
@Component({template: `<div [routerLink]="link" tabindex="1"></div>`})
|
||||
class LinkComponent {
|
||||
link: string|null|undefined = '/';
|
||||
link: string | null | undefined = '/';
|
||||
}
|
||||
TestBed.configureTestingModule(
|
||||
{imports: [RouterModule.forRoot([])], declarations: [LinkComponent]});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([])],
|
||||
declarations: [LinkComponent],
|
||||
});
|
||||
const fixture = TestBed.createComponent(LinkComponent);
|
||||
fixture.detectChanges();
|
||||
const link = fixture.debugElement.query(By.css('div')).nativeElement;
|
||||
|
|
@ -37,10 +39,10 @@ describe('RouterLink', () => {
|
|||
[preserveFragment]="preserveFragment"
|
||||
[skipLocationChange]="skipLocationChange"
|
||||
[replaceUrl]="replaceUrl"></div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
class LinkComponent {
|
||||
link: string|null|undefined = '/';
|
||||
link: string | null | undefined = '/';
|
||||
preserveFragment: unknown;
|
||||
skipLocationChange: unknown;
|
||||
replaceUrl: unknown;
|
||||
|
|
@ -117,10 +119,10 @@ describe('RouterLink', () => {
|
|||
[preserveFragment]="preserveFragment"
|
||||
[skipLocationChange]="skipLocationChange"
|
||||
[replaceUrl]="replaceUrl"></a>
|
||||
`
|
||||
`,
|
||||
})
|
||||
class LinkComponent {
|
||||
link: string|null|undefined = '/';
|
||||
link: string | null | undefined = '/';
|
||||
preserveFragment: unknown;
|
||||
skipLocationChange: unknown;
|
||||
replaceUrl: unknown;
|
||||
|
|
@ -179,8 +181,7 @@ describe('RouterLink', () => {
|
|||
|
||||
it('should handle routerLink in svg templates', () => {
|
||||
@Component({template: `<svg><a routerLink="test"></a></svg>`})
|
||||
class LinkComponent {
|
||||
}
|
||||
class LinkComponent {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([])],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@ import {RouterScroller} from '../src/router_scroller';
|
|||
// TODO: add tests that exercise the `withInMemoryScrolling` feature of the provideRouter function
|
||||
const fakeZone = {
|
||||
runOutsideAngular: (fn: any) => fn(),
|
||||
run: (fn: any) => fn()
|
||||
run: (fn: any) => fn(),
|
||||
};
|
||||
describe('RouterScroller', () => {
|
||||
it('defaults to disabled', () => {
|
||||
|
|
@ -25,29 +25,42 @@ describe('RouterScroller', () => {
|
|||
const router = <any>{
|
||||
events,
|
||||
parseUrl: (url: any) => new DefaultUrlSerializer().parse(url),
|
||||
triggerEvent: (e: any) => events.next(e)
|
||||
triggerEvent: (e: any) => events.next(e),
|
||||
};
|
||||
|
||||
const viewportScroller = jasmine.createSpyObj(
|
||||
'viewportScroller',
|
||||
['getScrollPosition', 'scrollToPosition', 'scrollToAnchor', 'setHistoryScrollRestoration']);
|
||||
const viewportScroller = jasmine.createSpyObj('viewportScroller', [
|
||||
'getScrollPosition',
|
||||
'scrollToPosition',
|
||||
'scrollToAnchor',
|
||||
'setHistoryScrollRestoration',
|
||||
]);
|
||||
setScroll(viewportScroller, 0, 0);
|
||||
const scroller = new RouterScroller(
|
||||
new DefaultUrlSerializer(), {events} as any, viewportScroller, fakeZone as any);
|
||||
new DefaultUrlSerializer(),
|
||||
{events} as any,
|
||||
viewportScroller,
|
||||
fakeZone as any,
|
||||
);
|
||||
|
||||
expect((scroller as any).options.scrollPositionRestoration).toBe('disabled');
|
||||
expect((scroller as any).options.anchorScrolling).toBe('disabled');
|
||||
});
|
||||
|
||||
function nextScrollEvent(events: Subject<Event>): Promise<Scroll> {
|
||||
return events.pipe(filter((e): e is Scroll => e instanceof Scroll), take(1)).toPromise() as
|
||||
Promise<Scroll>;
|
||||
return events
|
||||
.pipe(
|
||||
filter((e): e is Scroll => e instanceof Scroll),
|
||||
take(1),
|
||||
)
|
||||
.toPromise() as Promise<Scroll>;
|
||||
}
|
||||
|
||||
describe('scroll to top', () => {
|
||||
it('should scroll to the top', async () => {
|
||||
const {events, viewportScroller} =
|
||||
createRouterScroller({scrollPositionRestoration: 'top', anchorScrolling: 'disabled'});
|
||||
const {events, viewportScroller} = createRouterScroller({
|
||||
scrollPositionRestoration: 'top',
|
||||
anchorScrolling: 'disabled',
|
||||
});
|
||||
|
||||
events.next(new NavigationStart(1, '/a'));
|
||||
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||
|
|
@ -68,8 +81,10 @@ describe('RouterScroller', () => {
|
|||
|
||||
describe('scroll to the stored position', () => {
|
||||
it('should scroll to the stored position on popstate', async () => {
|
||||
const {events, viewportScroller} =
|
||||
createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'disabled'});
|
||||
const {events, viewportScroller} = createRouterScroller({
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'disabled',
|
||||
});
|
||||
|
||||
events.next(new NavigationStart(1, '/a'));
|
||||
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||
|
|
@ -92,8 +107,10 @@ describe('RouterScroller', () => {
|
|||
|
||||
describe('anchor scrolling', () => {
|
||||
it('should work (scrollPositionRestoration is disabled)', async () => {
|
||||
const {events, viewportScroller} =
|
||||
createRouterScroller({scrollPositionRestoration: 'disabled', anchorScrolling: 'enabled'});
|
||||
const {events, viewportScroller} = createRouterScroller({
|
||||
scrollPositionRestoration: 'disabled',
|
||||
anchorScrolling: 'enabled',
|
||||
});
|
||||
events.next(new NavigationStart(1, '/a#anchor'));
|
||||
events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor'));
|
||||
await nextScrollEvent(events);
|
||||
|
|
@ -114,8 +131,10 @@ describe('RouterScroller', () => {
|
|||
});
|
||||
|
||||
it('should work (scrollPositionRestoration is enabled)', async () => {
|
||||
const {events, viewportScroller} =
|
||||
createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled'});
|
||||
const {events, viewportScroller} = createRouterScroller({
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
});
|
||||
events.next(new NavigationStart(1, '/a#anchor'));
|
||||
events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor'));
|
||||
await nextScrollEvent(events);
|
||||
|
|
@ -138,68 +157,82 @@ describe('RouterScroller', () => {
|
|||
|
||||
describe('extending a scroll service', () => {
|
||||
it('work', fakeAsync(() => {
|
||||
const {events, viewportScroller} = createRouterScroller(
|
||||
{scrollPositionRestoration: 'disabled', anchorScrolling: 'disabled'});
|
||||
const {events, viewportScroller} = createRouterScroller({
|
||||
scrollPositionRestoration: 'disabled',
|
||||
anchorScrolling: 'disabled',
|
||||
});
|
||||
|
||||
events
|
||||
.pipe(filter((e): e is Scroll => e instanceof Scroll && !!e.position), switchMap(p => {
|
||||
// can be any delay (e.g., we can wait for NgRx store to emit an event)
|
||||
const r = new Subject<Scroll>();
|
||||
setTimeout(() => {
|
||||
r.next(p);
|
||||
r.complete();
|
||||
}, 1000);
|
||||
return r;
|
||||
}))
|
||||
.subscribe((e: Scroll) => {
|
||||
viewportScroller.scrollToPosition(e.position);
|
||||
});
|
||||
events
|
||||
.pipe(
|
||||
filter((e): e is Scroll => e instanceof Scroll && !!e.position),
|
||||
switchMap((p) => {
|
||||
// can be any delay (e.g., we can wait for NgRx store to emit an event)
|
||||
const r = new Subject<Scroll>();
|
||||
setTimeout(() => {
|
||||
r.next(p);
|
||||
r.complete();
|
||||
}, 1000);
|
||||
return r;
|
||||
}),
|
||||
)
|
||||
.subscribe((e: Scroll) => {
|
||||
viewportScroller.scrollToPosition(e.position);
|
||||
});
|
||||
|
||||
events.next(new NavigationStart(1, '/a'));
|
||||
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 10, 100);
|
||||
events.next(new NavigationStart(1, '/a'));
|
||||
events.next(new NavigationEnd(1, '/a', '/a'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 10, 100);
|
||||
|
||||
events.next(new NavigationStart(2, '/b'));
|
||||
events.next(new NavigationEnd(2, '/b', '/b'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 20, 200);
|
||||
events.next(new NavigationStart(2, '/b'));
|
||||
events.next(new NavigationEnd(2, '/b', '/b'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 20, 200);
|
||||
|
||||
events.next(new NavigationStart(3, '/c'));
|
||||
events.next(new NavigationEnd(3, '/c', '/c'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 30, 300);
|
||||
events.next(new NavigationStart(3, '/c'));
|
||||
events.next(new NavigationEnd(3, '/c', '/c'));
|
||||
tick();
|
||||
setScroll(viewportScroller, 30, 300);
|
||||
|
||||
events.next(new NavigationStart(4, '/a', 'popstate', {navigationId: 1}));
|
||||
events.next(new NavigationEnd(4, '/a', '/a'));
|
||||
events.next(new NavigationStart(4, '/a', 'popstate', {navigationId: 1}));
|
||||
events.next(new NavigationEnd(4, '/a', '/a'));
|
||||
|
||||
tick(500);
|
||||
expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled();
|
||||
tick(500);
|
||||
expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled();
|
||||
|
||||
events.next(new NavigationStart(5, '/a', 'popstate', {navigationId: 1}));
|
||||
events.next(new NavigationEnd(5, '/a', '/a'));
|
||||
events.next(new NavigationStart(5, '/a', 'popstate', {navigationId: 1}));
|
||||
events.next(new NavigationEnd(5, '/a', '/a'));
|
||||
|
||||
tick(5000);
|
||||
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]);
|
||||
}));
|
||||
tick(5000);
|
||||
expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
function createRouterScroller({scrollPositionRestoration, anchorScrolling}: {
|
||||
scrollPositionRestoration: 'disabled'|'enabled'|'top',
|
||||
anchorScrolling: 'disabled'|'enabled'
|
||||
function createRouterScroller({
|
||||
scrollPositionRestoration,
|
||||
anchorScrolling,
|
||||
}: {
|
||||
scrollPositionRestoration: 'disabled' | 'enabled' | 'top';
|
||||
anchorScrolling: 'disabled' | 'enabled';
|
||||
}) {
|
||||
const events = new Subject<Event>();
|
||||
const transitions: any = {events};
|
||||
|
||||
const viewportScroller = jasmine.createSpyObj(
|
||||
'viewportScroller',
|
||||
['getScrollPosition', 'scrollToPosition', 'scrollToAnchor', 'setHistoryScrollRestoration']);
|
||||
const viewportScroller = jasmine.createSpyObj('viewportScroller', [
|
||||
'getScrollPosition',
|
||||
'scrollToPosition',
|
||||
'scrollToAnchor',
|
||||
'setHistoryScrollRestoration',
|
||||
]);
|
||||
setScroll(viewportScroller, 0, 0);
|
||||
|
||||
const scroller = new RouterScroller(
|
||||
new DefaultUrlSerializer(), transitions, viewportScroller, fakeZone as any,
|
||||
{scrollPositionRestoration, anchorScrolling});
|
||||
new DefaultUrlSerializer(),
|
||||
transitions,
|
||||
viewportScroller,
|
||||
fakeZone as any,
|
||||
{scrollPositionRestoration, anchorScrolling},
|
||||
);
|
||||
scroller.init();
|
||||
|
||||
return {events, viewportScroller};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@
|
|||
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute, equalParamsAndUrlSegments, RouterState, RouterStateSnapshot} from '../src/router_state';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ActivatedRouteSnapshot,
|
||||
advanceActivatedRoute,
|
||||
equalParamsAndUrlSegments,
|
||||
RouterState,
|
||||
RouterStateSnapshot,
|
||||
} from '../src/router_state';
|
||||
import {Params, RouteTitleKey} from '../src/shared';
|
||||
import {UrlSegment} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
|
|
@ -105,14 +112,27 @@ describe('RouterState & Snapshot', () => {
|
|||
describe('equalParamsAndUrlSegments', () => {
|
||||
function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot {
|
||||
const snapshot = new (ActivatedRouteSnapshot as any)(
|
||||
url, params, null, null, null, null, null, null, null, -1, null);
|
||||
url,
|
||||
params,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null,
|
||||
);
|
||||
snapshot._routerState = new (RouterStateSnapshot as any)('', new TreeNode(snapshot, []));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function createSnapshotPairWithParent(
|
||||
params: [Params, Params], parentParams: [Params, Params],
|
||||
urls: [string, string]): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] {
|
||||
params: [Params, Params],
|
||||
parentParams: [Params, Params],
|
||||
urls: [string, string],
|
||||
): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] {
|
||||
const snapshot1 = createSnapshot(params[0], []);
|
||||
const snapshot2 = createSnapshot(params[1], []);
|
||||
|
||||
|
|
@ -120,49 +140,67 @@ describe('RouterState & Snapshot', () => {
|
|||
const snapshot2Parent = createSnapshot(parentParams[1], [new UrlSegment(urls[1], {})]);
|
||||
|
||||
(snapshot1 as any)._routerState = new (RouterStateSnapshot as any)(
|
||||
'', new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])]));
|
||||
'',
|
||||
new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])]),
|
||||
);
|
||||
(snapshot2 as any)._routerState = new (RouterStateSnapshot as any)(
|
||||
'', new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])]));
|
||||
'',
|
||||
new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])]),
|
||||
);
|
||||
|
||||
return [snapshot1, snapshot2];
|
||||
}
|
||||
|
||||
it('should return false when params are different', () => {
|
||||
expect(equalParamsAndUrlSegments(createSnapshot({a: '1'}, []), createSnapshot({a: '2'}, [])))
|
||||
.toEqual(false);
|
||||
expect(
|
||||
equalParamsAndUrlSegments(createSnapshot({a: '1'}, []), createSnapshot({a: '2'}, [])),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when urls are different', () => {
|
||||
expect(equalParamsAndUrlSegments(
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: '1'}, [new UrlSegment('b', {})])))
|
||||
.toEqual(false);
|
||||
expect(
|
||||
equalParamsAndUrlSegments(
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: '1'}, [new UrlSegment('b', {})]),
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true othewise', () => {
|
||||
expect(equalParamsAndUrlSegments(
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})])))
|
||||
.toEqual(true);
|
||||
expect(
|
||||
equalParamsAndUrlSegments(
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: '1'}, [new UrlSegment('a', {})]),
|
||||
),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false when upstream params are different', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {c: '1'}], ['a', 'a']);
|
||||
const [snapshot1, snapshot2] = createSnapshotPairWithParent(
|
||||
[{a: '1'}, {a: '1'}],
|
||||
[{b: '1'}, {c: '1'}],
|
||||
['a', 'a'],
|
||||
);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when upstream urls are different', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {b: '1'}], ['a', 'b']);
|
||||
const [snapshot1, snapshot2] = createSnapshotPairWithParent(
|
||||
[{a: '1'}, {a: '1'}],
|
||||
[{b: '1'}, {b: '1'}],
|
||||
['a', 'b'],
|
||||
);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true when upstream urls and params are equal', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {b: '1'}], ['a', 'a']);
|
||||
const [snapshot1, snapshot2] = createSnapshotPairWithParent(
|
||||
[{a: '1'}, {a: '1'}],
|
||||
[{b: '1'}, {b: '1'}],
|
||||
['a', 'a'],
|
||||
);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(true);
|
||||
});
|
||||
|
|
@ -180,7 +218,18 @@ describe('RouterState & Snapshot', () => {
|
|||
const fragment = '';
|
||||
const data = {};
|
||||
const snapshot = new (ActivatedRouteSnapshot as any)(
|
||||
url, params, queryParams, fragment, data, null, null, null, null, -1, null);
|
||||
url,
|
||||
params,
|
||||
queryParams,
|
||||
fragment,
|
||||
data,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null,
|
||||
);
|
||||
const state = new (RouterStateSnapshot as any)('', new TreeNode(snapshot, []));
|
||||
snapshot._routerState = state;
|
||||
return snapshot;
|
||||
|
|
@ -206,12 +255,23 @@ describe('RouterState & Snapshot', () => {
|
|||
const data = {[RouteTitleKey]: 'resolved title'};
|
||||
const route = createActivatedRoute('a');
|
||||
const snapshot = new (ActivatedRouteSnapshot as any)(
|
||||
[], null, null, null, data, null, 'test', null, null, -1, null!);
|
||||
let resolvedTitle: string|undefined;
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
data,
|
||||
null,
|
||||
'test',
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null!,
|
||||
);
|
||||
let resolvedTitle: string | undefined;
|
||||
|
||||
route.data.next(data);
|
||||
|
||||
route.title.forEach((title: string|undefined) => {
|
||||
route.title.forEach((title: string | undefined) => {
|
||||
resolvedTitle = title;
|
||||
});
|
||||
|
||||
|
|
@ -223,11 +283,29 @@ describe('RouterState & Snapshot', () => {
|
|||
|
||||
function createActivatedRouteSnapshot(cmp: string) {
|
||||
return new (ActivatedRouteSnapshot as any)(
|
||||
[], null, null, null, null, null, cmp, null, null, -1, null!);
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
cmp,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null!,
|
||||
);
|
||||
}
|
||||
|
||||
function createActivatedRoute(cmp: string) {
|
||||
return new (ActivatedRoute as any)(
|
||||
new BehaviorSubject([new UrlSegment('', {})]), new BehaviorSubject({}), null, null,
|
||||
new BehaviorSubject({}), null, cmp, null);
|
||||
new BehaviorSubject([new UrlSegment('', {})]),
|
||||
new BehaviorSubject({}),
|
||||
null,
|
||||
null,
|
||||
new BehaviorSubject({}),
|
||||
null,
|
||||
cmp,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,13 +43,12 @@ describe('ParamsMap', () => {
|
|||
expect(map.getAll('name')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not error when trying to call ParamMap.get function using an object created with Object.create() function',
|
||||
() => {
|
||||
const objectToMap: Params = Object.create(null);
|
||||
objectToMap['single'] = 's';
|
||||
objectToMap['multiple'] = ['m1', 'm2'];
|
||||
const paramMaps: ParamMap = convertToParamMap(objectToMap);
|
||||
expect(() => paramMaps.get('single')).not.toThrow();
|
||||
expect(paramMaps.get('single')).toEqual('s');
|
||||
});
|
||||
it('should not error when trying to call ParamMap.get function using an object created with Object.create() function', () => {
|
||||
const objectToMap: Params = Object.create(null);
|
||||
objectToMap['single'] = 's';
|
||||
objectToMap['multiple'] = ['m1', 'm2'];
|
||||
const paramMaps: ParamMap = convertToParamMap(objectToMap);
|
||||
expect(() => paramMaps.get('single')).not.toThrow();
|
||||
expect(paramMaps.get('single')).toEqual('s');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,354 +12,375 @@ import {By} from '@angular/platform-browser';
|
|||
import {provideRoutes, Router, RouterModule, ROUTES} from '@angular/router';
|
||||
|
||||
@Component({template: '<div>simple standalone</div>', standalone: true})
|
||||
export class SimpleStandaloneComponent {
|
||||
}
|
||||
export class SimpleStandaloneComponent {}
|
||||
|
||||
@Component({template: '<div>not standalone</div>', standalone: false})
|
||||
export class NotStandaloneComponent {
|
||||
}
|
||||
export class NotStandaloneComponent {}
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
})
|
||||
export class RootCmp {
|
||||
}
|
||||
export class RootCmp {}
|
||||
|
||||
describe('standalone in Router API', () => {
|
||||
describe('loadChildren => routes', () => {
|
||||
it('can navigate to and render standalone component', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{
|
||||
path: 'lazy',
|
||||
component: RootCmp,
|
||||
loadChildren: () => [{path: '', component: SimpleStandaloneComponent}],
|
||||
},
|
||||
])]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'lazy',
|
||||
component: RootCmp,
|
||||
loadChildren: () => [{path: '', component: SimpleStandaloneComponent}],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/lazy');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
}));
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/lazy');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
}));
|
||||
|
||||
it('throws an error when loadChildren=>routes has a component that is not standalone',
|
||||
fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([
|
||||
{
|
||||
path: 'lazy',
|
||||
component: RootCmp,
|
||||
loadChildren: () => [{path: 'notstandalone', component: NotStandaloneComponent}],
|
||||
},
|
||||
])]
|
||||
});
|
||||
it('throws an error when loadChildren=>routes has a component that is not standalone', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'lazy',
|
||||
component: RootCmp,
|
||||
loadChildren: () => [{path: 'notstandalone', component: NotStandaloneComponent}],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/lazy/notstandalone');
|
||||
expect(() => advance(root))
|
||||
.toThrowError(/.*lazy\/notstandalone.*component must be standalone/);
|
||||
}));
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/lazy/notstandalone');
|
||||
expect(() => advance(root)).toThrowError(
|
||||
/.*lazy\/notstandalone.*component must be standalone/,
|
||||
);
|
||||
}));
|
||||
});
|
||||
describe('route providers', () => {
|
||||
it('can provide a guard on a route', fakeAsync(() => {
|
||||
@Injectable()
|
||||
class ConfigurableGuard {
|
||||
static canActivateValue = false;
|
||||
canActivate() {
|
||||
return ConfigurableGuard.canActivateValue;
|
||||
}
|
||||
}
|
||||
@Injectable()
|
||||
class ConfigurableGuard {
|
||||
static canActivateValue = false;
|
||||
canActivate() {
|
||||
return ConfigurableGuard.canActivateValue;
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{
|
||||
path: 'simple',
|
||||
providers: [ConfigurableGuard],
|
||||
canActivate: [ConfigurableGuard],
|
||||
component: SimpleStandaloneComponent
|
||||
}]),
|
||||
],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'simple',
|
||||
providers: [ConfigurableGuard],
|
||||
canActivate: [ConfigurableGuard],
|
||||
component: SimpleStandaloneComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
ConfigurableGuard.canActivateValue = false;
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).not.toContain('simple standalone');
|
||||
expect(router.url).not.toContain('simple');
|
||||
ConfigurableGuard.canActivateValue = false;
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).not.toContain('simple standalone');
|
||||
expect(router.url).not.toContain('simple');
|
||||
|
||||
ConfigurableGuard.canActivateValue = true;
|
||||
router.navigateByUrl('/simple');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
expect(router.url).toContain('simple');
|
||||
}));
|
||||
ConfigurableGuard.canActivateValue = true;
|
||||
router.navigateByUrl('/simple');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
expect(router.url).toContain('simple');
|
||||
}));
|
||||
|
||||
it('can inject provider on a route into component', fakeAsync(() => {
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{path: 'home', providers: [Service], component: MyComponent}]),
|
||||
],
|
||||
declarations: [MyComponent],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{path: 'home', providers: [Service], component: MyComponent}]),
|
||||
],
|
||||
declarations: [MyComponent],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('my service');
|
||||
expect(router.url).toContain('home');
|
||||
}));
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('my service');
|
||||
expect(router.url).toContain('home');
|
||||
}));
|
||||
|
||||
it('can not inject provider in lazy loaded ngModule from component on same level',
|
||||
fakeAsync(() => {
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
it('can not inject provider in lazy loaded ngModule from component on same level', fakeAsync(() => {
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
|
||||
@NgModule({providers: [Service]})
|
||||
class LazyModule {
|
||||
}
|
||||
@NgModule({providers: [Service]})
|
||||
class LazyModule {}
|
||||
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(
|
||||
[{path: 'home', loadChildren: () => LazyModule, component: MyComponent}]),
|
||||
],
|
||||
declarations: [MyComponent],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{path: 'home', loadChildren: () => LazyModule, component: MyComponent},
|
||||
]),
|
||||
],
|
||||
declarations: [MyComponent],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError();
|
||||
}));
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError();
|
||||
}));
|
||||
|
||||
it('component from lazy module can inject provider from parent route', fakeAsync(() => {
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
@Injectable()
|
||||
class Service {
|
||||
value = 'my service';
|
||||
}
|
||||
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
@NgModule({
|
||||
providers: [Service],
|
||||
declarations: [MyComponent],
|
||||
imports: [RouterModule.forChild([{path: '', component: MyComponent}])]
|
||||
})
|
||||
class LazyModule {
|
||||
}
|
||||
@Component({template: `{{service.value}}`})
|
||||
class MyComponent {
|
||||
constructor(readonly service: Service) {}
|
||||
}
|
||||
@NgModule({
|
||||
providers: [Service],
|
||||
declarations: [MyComponent],
|
||||
imports: [RouterModule.forChild([{path: '', component: MyComponent}])],
|
||||
})
|
||||
class LazyModule {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{path: 'home', loadChildren: () => LazyModule}])],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{path: 'home', loadChildren: () => LazyModule}]),
|
||||
],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('my service');
|
||||
}));
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('my service');
|
||||
}));
|
||||
it('gets the correct injector for guards and components when combining lazy modules and route providers', fakeAsync(() => {
|
||||
const canActivateLog: string[] = [];
|
||||
abstract class ServiceBase {
|
||||
abstract name: string;
|
||||
canActivate() {
|
||||
canActivateLog.push(this.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
it('gets the correct injector for guards and components when combining lazy modules and route providers',
|
||||
fakeAsync(() => {
|
||||
const canActivateLog: string[] = [];
|
||||
abstract class ServiceBase {
|
||||
abstract name: string;
|
||||
canActivate() {
|
||||
canActivateLog.push(this.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@Injectable()
|
||||
class Service1 extends ServiceBase {
|
||||
override name = 'service1';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class Service1 extends ServiceBase {
|
||||
override name = 'service1';
|
||||
}
|
||||
@Injectable()
|
||||
class Service2 extends ServiceBase {
|
||||
override name = 'service2';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class Service2 extends ServiceBase {
|
||||
override name = 'service2';
|
||||
}
|
||||
@Injectable()
|
||||
class Service3 extends ServiceBase {
|
||||
override name = 'service3';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class Service3 extends ServiceBase {
|
||||
override name = 'service3';
|
||||
}
|
||||
@Component({template: `parent<router-outlet></router-outlet>`})
|
||||
class ParentCmp {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
@Component({template: `child`})
|
||||
class ChildCmp {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
|
||||
@Component({template: `parent<router-outlet></router-outlet>`})
|
||||
class ParentCmp {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
@Component({template: `child`})
|
||||
class ChildCmp {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
@Component({template: `child2`})
|
||||
class ChildCmp2 {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
@NgModule({
|
||||
providers: [{provide: ServiceBase, useClass: Service2}],
|
||||
declarations: [ChildCmp, ChildCmp2],
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
// This component and guard should get Service2 since it's provided in this module
|
||||
component: ChildCmp,
|
||||
canActivate: [ServiceBase],
|
||||
},
|
||||
{
|
||||
path: 'child2',
|
||||
providers: [{provide: ServiceBase, useFactory: () => new Service3()}],
|
||||
// This component and guard should get Service3 since it's provided on this route
|
||||
component: ChildCmp2,
|
||||
canActivate: [ServiceBase],
|
||||
},
|
||||
]),
|
||||
],
|
||||
})
|
||||
class LazyModule {}
|
||||
|
||||
@Component({template: `child2`})
|
||||
class ChildCmp2 {
|
||||
constructor(readonly service: ServiceBase) {}
|
||||
}
|
||||
@NgModule({
|
||||
providers: [{provide: ServiceBase, useClass: Service2}],
|
||||
declarations: [ChildCmp, ChildCmp2],
|
||||
imports: [RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
// This component and guard should get Service2 since it's provided in this module
|
||||
component: ChildCmp,
|
||||
canActivate: [ServiceBase],
|
||||
},
|
||||
{
|
||||
path: 'child2',
|
||||
providers: [{provide: ServiceBase, useFactory: () => new Service3()}],
|
||||
// This component and guard should get Service3 since it's provided on this route
|
||||
component: ChildCmp2,
|
||||
canActivate: [ServiceBase],
|
||||
},
|
||||
])]
|
||||
})
|
||||
class LazyModule {
|
||||
}
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
// This component and guard should get Service1 since it's provided on this route
|
||||
component: ParentCmp,
|
||||
canActivate: [ServiceBase],
|
||||
providers: [{provide: ServiceBase, useFactory: () => new Service1()}],
|
||||
loadChildren: () => LazyModule,
|
||||
},
|
||||
]),
|
||||
],
|
||||
declarations: [ParentCmp],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(canActivateLog).toEqual(['service1', 'service2']);
|
||||
expect(
|
||||
root.debugElement.query(By.directive(ParentCmp)).componentInstance.service.name,
|
||||
).toEqual('service1');
|
||||
expect(
|
||||
root.debugElement.query(By.directive(ChildCmp)).componentInstance.service.name,
|
||||
).toEqual('service2');
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
// This component and guard should get Service1 since it's provided on this route
|
||||
component: ParentCmp,
|
||||
canActivate: [ServiceBase],
|
||||
providers: [{provide: ServiceBase, useFactory: () => new Service1()}],
|
||||
loadChildren: () => LazyModule
|
||||
}]),
|
||||
],
|
||||
declarations: [ParentCmp],
|
||||
});
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(canActivateLog).toEqual(['service1', 'service2']);
|
||||
expect(root.debugElement.query(By.directive(ParentCmp)).componentInstance.service.name)
|
||||
.toEqual('service1');
|
||||
expect(root.debugElement.query(By.directive(ChildCmp)).componentInstance.service.name)
|
||||
.toEqual('service2');
|
||||
|
||||
router.navigateByUrl('/home/child2');
|
||||
advance(root);
|
||||
expect(canActivateLog).toEqual(['service1', 'service2', 'service3']);
|
||||
expect(root.debugElement.query(By.directive(ChildCmp2)).componentInstance.service.name)
|
||||
.toEqual('service3');
|
||||
}));
|
||||
router.navigateByUrl('/home/child2');
|
||||
advance(root);
|
||||
expect(canActivateLog).toEqual(['service1', 'service2', 'service3']);
|
||||
expect(
|
||||
root.debugElement.query(By.directive(ChildCmp2)).componentInstance.service.name,
|
||||
).toEqual('service3');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('loadComponent', () => {
|
||||
it('does not load component when canActivate returns false', fakeAsync(() => {
|
||||
const loadComponentSpy = jasmine.createSpy();
|
||||
@Injectable({providedIn: 'root'})
|
||||
class Guard {
|
||||
canActivate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const loadComponentSpy = jasmine.createSpy();
|
||||
@Injectable({providedIn: 'root'})
|
||||
class Guard {
|
||||
canActivate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadComponent: loadComponentSpy,
|
||||
canActivate: [Guard],
|
||||
}])]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: loadComponentSpy,
|
||||
canActivate: [Guard],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
tick();
|
||||
expect(loadComponentSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
tick();
|
||||
expect(loadComponentSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('loads and renders lazy component', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadComponent: () => SimpleStandaloneComponent,
|
||||
}])],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: () => SimpleStandaloneComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
}));
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
advance(root);
|
||||
expect(root.nativeElement.innerHTML).toContain('simple standalone');
|
||||
}));
|
||||
|
||||
it('throws error when loadComponent is not standalone', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadComponent: () => NotStandaloneComponent,
|
||||
}])],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: () => NotStandaloneComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError(/.*home.*component must be standalone/);
|
||||
}));
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError(/.*home.*component must be standalone/);
|
||||
}));
|
||||
|
||||
it('throws error when loadComponent is used with a module', fakeAsync(() => {
|
||||
@NgModule()
|
||||
class LazyModule {
|
||||
}
|
||||
@NgModule()
|
||||
class LazyModule {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadComponent: () => LazyModule,
|
||||
}])],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: () => LazyModule,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError(/.*home.*Use 'loadChildren' instead/);
|
||||
}));
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
TestBed.inject(Router).navigateByUrl('/home');
|
||||
expect(() => advance(root)).toThrowError(/.*home.*Use 'loadChildren' instead/);
|
||||
}));
|
||||
});
|
||||
describe('default export unwrapping', () => {
|
||||
it('should work for loadComponent', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadComponent: () => import('./default_export_component'),
|
||||
}])],
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: () => import('./default_export_component'),
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
|
@ -371,10 +392,14 @@ describe('standalone in Router API', () => {
|
|||
|
||||
it('should work for loadChildren', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterModule.forRoot([{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./default_export_routes'),
|
||||
}])],
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./default_export_routes'),
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const root = TestBed.createComponent(RootCmp);
|
||||
|
|
@ -387,13 +412,12 @@ describe('standalone in Router API', () => {
|
|||
});
|
||||
|
||||
describe('provideRoutes', () => {
|
||||
it('warns if provideRoutes is used without provideRouter, RouterModule, or RouterModule.forRoot',
|
||||
() => {
|
||||
spyOn(console, 'warn');
|
||||
TestBed.configureTestingModule({providers: [provideRoutes([])]});
|
||||
TestBed.inject(ROUTES);
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
});
|
||||
it('warns if provideRoutes is used without provideRouter, RouterModule, or RouterModule.forRoot', () => {
|
||||
spyOn(console, 'warn');
|
||||
TestBed.configureTestingModule({providers: [provideRoutes([])]});
|
||||
TestBed.inject(ROUTES);
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function advance(fixture: ComponentFixture<unknown>) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@
|
|||
*/
|
||||
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, encodeUriFragment, encodeUriQuery, encodeUriSegment, serializePath, UrlSegmentGroup} from '../src/url_tree';
|
||||
import {
|
||||
DefaultUrlSerializer,
|
||||
encodeUriFragment,
|
||||
encodeUriQuery,
|
||||
encodeUriSegment,
|
||||
serializePath,
|
||||
UrlSegmentGroup,
|
||||
} from '../src/url_tree';
|
||||
|
||||
describe('url serializer', () => {
|
||||
const url = new DefaultUrlSerializer();
|
||||
|
|
@ -60,8 +67,9 @@ describe('url serializer', () => {
|
|||
const tree = url.parse('/path/to/something;query=file=test;query2=test2');
|
||||
expect(tree.root.children['primary'].segments[2].path).toEqual('something');
|
||||
expect(tree.root.children['primary'].segments[2].parameterMap.keys).toHaveSize(2);
|
||||
expect(tree.root.children['primary'].segments[2].parameterMap.get('query'))
|
||||
.toEqual('file=test');
|
||||
expect(tree.root.children['primary'].segments[2].parameterMap.get('query')).toEqual(
|
||||
'file=test',
|
||||
);
|
||||
expect(tree.root.children['primary'].segments[2].parameterMap.get('query2')).toEqual('test2');
|
||||
});
|
||||
|
||||
|
|
@ -86,8 +94,9 @@ describe('url serializer', () => {
|
|||
});
|
||||
|
||||
it('should not parse empty path segments with params', () => {
|
||||
expect(() => url.parse('/one/two/(;a=1//right:;b=2)'))
|
||||
.toThrowError(/Empty path url segment cannot have parameters/);
|
||||
expect(() => url.parse('/one/two/(;a=1//right:;b=2)')).toThrowError(
|
||||
/Empty path url segment cannot have parameters/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse scoped secondary segments', () => {
|
||||
|
|
@ -225,19 +234,23 @@ describe('url serializer', () => {
|
|||
|
||||
describe('encoding/decoding', () => {
|
||||
it('should encode/decode path segments and parameters', () => {
|
||||
const u = `/${encodeUriSegment('one two')};${encodeUriSegment('p 1')}=${
|
||||
encodeUriSegment('v 1')};${encodeUriSegment('p 2')}=${encodeUriSegment('v 2')}`;
|
||||
const u = `/${encodeUriSegment('one two')};${encodeUriSegment('p 1')}=${encodeUriSegment(
|
||||
'v 1',
|
||||
)};${encodeUriSegment('p 2')}=${encodeUriSegment('v 2')}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters)
|
||||
.toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({
|
||||
['p 1']: 'v 1',
|
||||
['p 2']: 'v 2',
|
||||
});
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
it('should encode/decode "slash" in path segments and parameters', () => {
|
||||
const u = `/${encodeUriSegment('one/two')};${encodeUriSegment('p/1')}=${
|
||||
encodeUriSegment('v/1')}/three`;
|
||||
const u = `/${encodeUriSegment('one/two')};${encodeUriSegment('p/1')}=${encodeUriSegment(
|
||||
'v/1',
|
||||
)}/three`;
|
||||
const tree = url.parse(u);
|
||||
const segment = tree.root.children[PRIMARY_OUTLET].segments[0];
|
||||
expect(segment.path).toEqual('one/two');
|
||||
|
|
@ -248,8 +261,9 @@ describe('url serializer', () => {
|
|||
});
|
||||
|
||||
it('should encode/decode query params', () => {
|
||||
const u = `/one?${encodeUriQuery('p 1')}=${encodeUriQuery('v 1')}&${encodeUriQuery('p 2')}=${
|
||||
encodeUriQuery('v 2')}`;
|
||||
const u = `/one?${encodeUriQuery('p 1')}=${encodeUriQuery('v 1')}&${encodeUriQuery(
|
||||
'p 2',
|
||||
)}=${encodeUriQuery('v 2')}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'});
|
||||
|
|
@ -277,7 +291,7 @@ describe('url serializer', () => {
|
|||
it('should encode query params leaving sub-delimiters intact', () => {
|
||||
const percentChars = '/?#&+=[] ';
|
||||
const percentCharsEncoded = '%2F%3F%23%26%2B%3D%5B%5D%20';
|
||||
const intactChars = '!$\'()*,;:';
|
||||
const intactChars = "!$'()*,;:";
|
||||
const params = percentChars + intactChars;
|
||||
const paramsEncoded = percentCharsEncoded + intactChars;
|
||||
const mixedCaseString = 'sTrInG';
|
||||
|
|
@ -308,7 +322,6 @@ describe('url serializer', () => {
|
|||
const auxParsed = url.parse(auxRoutesUrl).root;
|
||||
const fooParsed = url.parse(fooValueUrl).root;
|
||||
|
||||
|
||||
// Test base case
|
||||
expect(auxParsed.children[PRIMARY_OUTLET].segments.length).toBe(1);
|
||||
expect(auxParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc');
|
||||
|
|
@ -320,7 +333,7 @@ describe('url serializer', () => {
|
|||
expect(fooParsed.children[PRIMARY_OUTLET].segments.length).toBe(1);
|
||||
expect(fooParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc');
|
||||
expect(fooParsed.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({
|
||||
foo: '(other:val)'
|
||||
foo: '(other:val)',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -371,18 +384,17 @@ describe('url serializer', () => {
|
|||
expect(url.serialize(parsed)).toBe(`/foo#${notEncoded}${encoded}`);
|
||||
});
|
||||
|
||||
it('should encode minimal special characters plus parens and semi-colon in matrix params',
|
||||
() => {
|
||||
const notEncoded = unreserved + `:@!$'*,&`;
|
||||
const encode = ` /%=#()[];?+`;
|
||||
const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F%2B`;
|
||||
it('should encode minimal special characters plus parens and semi-colon in matrix params', () => {
|
||||
const notEncoded = unreserved + `:@!$'*,&`;
|
||||
const encode = ` /%=#()[];?+`;
|
||||
const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F%2B`;
|
||||
|
||||
const parsed = url.parse('/foo');
|
||||
const parsed = url.parse('/foo');
|
||||
|
||||
parsed.root.children[PRIMARY_OUTLET].segments[0].parameters = {notEncoded, encode};
|
||||
parsed.root.children[PRIMARY_OUTLET].segments[0].parameters = {notEncoded, encode};
|
||||
|
||||
expect(url.serialize(parsed)).toBe(`/foo;notEncoded=${notEncoded};encode=${encoded}`);
|
||||
});
|
||||
expect(url.serialize(parsed)).toBe(`/foo;notEncoded=${notEncoded};encode=${encoded}`);
|
||||
});
|
||||
|
||||
it('should encode special characters in the path the same as matrix params', () => {
|
||||
const notEncoded = unreserved + `:@!$'*,&`;
|
||||
|
|
@ -417,11 +429,14 @@ describe('url serializer', () => {
|
|||
});
|
||||
|
||||
function expectSegment(
|
||||
segment: UrlSegmentGroup, expected: string, hasChildren: boolean = false): void {
|
||||
if (segment.segments.filter(s => s.path === '').length > 0) {
|
||||
segment: UrlSegmentGroup,
|
||||
expected: string,
|
||||
hasChildren: boolean = false,
|
||||
): void {
|
||||
if (segment.segments.filter((s) => s.path === '').length > 0) {
|
||||
throw new Error(`UrlSegments cannot be empty ${segment.segments}`);
|
||||
}
|
||||
const p = segment.segments.map(p => serializePath(p)).join('/');
|
||||
const p = segment.segments.map((p) => serializePath(p)).join('/');
|
||||
expect(p).toEqual(expected);
|
||||
expect(Object.keys(segment.children).length > 0).toEqual(hasChildren);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,13 +254,15 @@ describe('UrlTree', () => {
|
|||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when matrix params match on subset of urlTree match ' +
|
||||
'with container paths split into multiple segments',
|
||||
() => {
|
||||
const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)');
|
||||
const t2 = serializer.parse('/one;a=1/two;b=2');
|
||||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
});
|
||||
it(
|
||||
'should return true when matrix params match on subset of urlTree match ' +
|
||||
'with container paths split into multiple segments',
|
||||
() => {
|
||||
const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)');
|
||||
const t2 = serializer.parse('/one;a=1/two;b=2');
|
||||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('subset match', () => {
|
||||
|
|
@ -290,13 +292,15 @@ describe('UrlTree', () => {
|
|||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when matrix params match on subset of urlTree match ' +
|
||||
'with container paths split into multiple segments',
|
||||
() => {
|
||||
const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)');
|
||||
const t2 = serializer.parse('/one;a=1/two');
|
||||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
});
|
||||
it(
|
||||
'should return true when matrix params match on subset of urlTree match ' +
|
||||
'with container paths split into multiple segments',
|
||||
() => {
|
||||
const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)');
|
||||
const t2 = serializer.parse('/one;a=1/two');
|
||||
expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ describe('tree', () => {
|
|||
});
|
||||
|
||||
it('should return the parent of a node (second child)', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(
|
||||
1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])])) as any;
|
||||
const t = new Tree<any>(
|
||||
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]),
|
||||
) as any;
|
||||
expect(t.parent(1)).toEqual(null);
|
||||
expect(t.parent(3)).toEqual(1);
|
||||
});
|
||||
|
|
@ -40,8 +41,9 @@ describe('tree', () => {
|
|||
});
|
||||
|
||||
it('should return the siblings of a node', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(
|
||||
1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])])) as any;
|
||||
const t = new Tree<any>(
|
||||
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]),
|
||||
) as any;
|
||||
expect(t.siblings(2)).toEqual([3]);
|
||||
expect(t.siblings(1)).toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,14 @@ import {DOCUMENT} from '@angular/common';
|
|||
import {Component, destroyPlatform, inject} from '@angular/core';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {withBody} from '@angular/private/testing';
|
||||
import {Event, NavigationEnd, provideRouter, Router, withDisabledInitialNavigation, withViewTransitions} from '@angular/router';
|
||||
|
||||
import {
|
||||
Event,
|
||||
NavigationEnd,
|
||||
provideRouter,
|
||||
Router,
|
||||
withDisabledInitialNavigation,
|
||||
withViewTransitions,
|
||||
} from '@angular/router';
|
||||
|
||||
describe('view transitions', () => {
|
||||
if (isNode) {
|
||||
|
|
@ -27,16 +33,17 @@ describe('view transitions', () => {
|
|||
standalone: true,
|
||||
template: ``,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
class App {}
|
||||
beforeEach(withBody('<test-app></test-app>', () => {}));
|
||||
it('should skip initial transition', async () => {
|
||||
const appRef = await bootstrapApplication(App, {
|
||||
providers: [provideRouter(
|
||||
providers: [
|
||||
provideRouter(
|
||||
[{path: '**', component: App}],
|
||||
withDisabledInitialNavigation(),
|
||||
withViewTransitions({skipInitialTransition: true}),
|
||||
)]
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const doc = appRef.injector.get(DOCUMENT);
|
||||
|
|
@ -57,16 +64,14 @@ describe('view transitions', () => {
|
|||
template: `b`,
|
||||
standalone: true,
|
||||
})
|
||||
class ComponentB {
|
||||
}
|
||||
class ComponentB {}
|
||||
|
||||
|
||||
const res = await bootstrapApplication(
|
||||
App,
|
||||
{providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())]});
|
||||
const res = await bootstrapApplication(App, {
|
||||
providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())],
|
||||
});
|
||||
const router = res.injector.get(Router);
|
||||
const eventLog = [] as Event[];
|
||||
router.events.subscribe(e => {
|
||||
router.events.subscribe((e) => {
|
||||
eventLog.push(e);
|
||||
});
|
||||
|
||||
|
|
@ -90,11 +95,13 @@ describe('view transitions', () => {
|
|||
|
||||
const transitionSpy = jasmine.createSpy();
|
||||
const appRef = await bootstrapApplication(App, {
|
||||
providers: [provideRouter(
|
||||
providers: [
|
||||
provideRouter(
|
||||
[{path: '**', component: App}],
|
||||
withDisabledInitialNavigation(),
|
||||
withViewTransitions({onViewTransitionCreated: transitionSpy}),
|
||||
)]
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const doc = appRef.injector.get(DOCUMENT);
|
||||
|
|
|
|||
|
|
@ -83,15 +83,15 @@ export class RouterTestingHarness {
|
|||
this.fixture.detectChanges();
|
||||
}
|
||||
/** The `DebugElement` of the `RouterOutlet` component. `null` if the outlet is not activated. */
|
||||
get routeDebugElement(): DebugElement|null {
|
||||
get routeDebugElement(): DebugElement | null {
|
||||
const outlet = (this.fixture.componentInstance as RootCmp).outlet;
|
||||
if (!outlet || !outlet.isActivated) {
|
||||
return null;
|
||||
}
|
||||
return this.fixture.debugElement.query(v => v.componentInstance === outlet.component);
|
||||
return this.fixture.debugElement.query((v) => v.componentInstance === outlet.component);
|
||||
}
|
||||
/** The native element of the `RouterOutlet` component. `null` if the outlet is not activated. */
|
||||
get routeNativeElement(): HTMLElement|null {
|
||||
get routeNativeElement(): HTMLElement | null {
|
||||
return this.routeDebugElement?.nativeElement ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ export class RouterTestingHarness {
|
|||
* @returns The activated component instance of the `RouterOutlet` after navigation completes
|
||||
* (`null` if the outlet does not get activated).
|
||||
*/
|
||||
async navigateByUrl(url: string): Promise<null|{}>;
|
||||
async navigateByUrl(url: string): Promise<null | {}>;
|
||||
/**
|
||||
* Triggers a router navigation and waits for it to complete.
|
||||
*
|
||||
|
|
@ -133,10 +133,10 @@ export class RouterTestingHarness {
|
|||
* @returns The activated component instance of the `RouterOutlet` after navigation completes.
|
||||
*/
|
||||
async navigateByUrl<T>(url: string, requiredRoutedComponentType: Type<T>): Promise<T>;
|
||||
async navigateByUrl<T>(url: string, requiredRoutedComponentType?: Type<T>): Promise<T|null> {
|
||||
async navigateByUrl<T>(url: string, requiredRoutedComponentType?: Type<T>): Promise<T | null> {
|
||||
const router = TestBed.inject(Router);
|
||||
let resolveFn!: () => void;
|
||||
const redirectTrackingPromise = new Promise<void>(resolve => {
|
||||
const redirectTrackingPromise = new Promise<void>((resolve) => {
|
||||
resolveFn = resolve;
|
||||
});
|
||||
afterNextNavigation(TestBed.inject(Router), resolveFn);
|
||||
|
|
@ -148,16 +148,20 @@ export class RouterTestingHarness {
|
|||
// rejects
|
||||
if (outlet && outlet.isActivated && outlet.activatedRoute.component) {
|
||||
const activatedComponent = outlet.component;
|
||||
if (requiredRoutedComponentType !== undefined &&
|
||||
!(activatedComponent instanceof requiredRoutedComponentType)) {
|
||||
throw new Error(`Unexpected routed component type. Expected ${
|
||||
requiredRoutedComponentType.name} but got ${activatedComponent.constructor.name}`);
|
||||
if (
|
||||
requiredRoutedComponentType !== undefined &&
|
||||
!(activatedComponent instanceof requiredRoutedComponentType)
|
||||
) {
|
||||
throw new Error(
|
||||
`Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but got ${activatedComponent.constructor.name}`,
|
||||
);
|
||||
}
|
||||
return activatedComponent as T;
|
||||
} else {
|
||||
if (requiredRoutedComponentType !== undefined) {
|
||||
throw new Error(`Unexpected routed component type. Expected ${
|
||||
requiredRoutedComponentType.name} but the navigation did not activate any component.`);
|
||||
throw new Error(
|
||||
`Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but the navigation did not activate any component.`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,27 @@
|
|||
import {Location} from '@angular/common';
|
||||
import {provideLocationMocks} from '@angular/common/testing';
|
||||
import {Compiler, inject, Injector, ModuleWithProviders, NgModule} from '@angular/core';
|
||||
import {ChildrenOutletContexts, ExtraOptions, NoPreloading, Route, Router, ROUTER_CONFIGURATION, RouteReuseStrategy, RouterModule, ROUTES, Routes, TitleStrategy, UrlHandlingStrategy, UrlSerializer, withPreloading, ɵROUTER_PROVIDERS as ROUTER_PROVIDERS} from '@angular/router';
|
||||
import {
|
||||
ChildrenOutletContexts,
|
||||
ExtraOptions,
|
||||
NoPreloading,
|
||||
Route,
|
||||
Router,
|
||||
ROUTER_CONFIGURATION,
|
||||
RouteReuseStrategy,
|
||||
RouterModule,
|
||||
ROUTES,
|
||||
Routes,
|
||||
TitleStrategy,
|
||||
UrlHandlingStrategy,
|
||||
UrlSerializer,
|
||||
withPreloading,
|
||||
ɵROUTER_PROVIDERS as ROUTER_PROVIDERS,
|
||||
} from '@angular/router';
|
||||
|
||||
function isUrlHandlingStrategy(opts: ExtraOptions|
|
||||
UrlHandlingStrategy): opts is UrlHandlingStrategy {
|
||||
function isUrlHandlingStrategy(
|
||||
opts: ExtraOptions | UrlHandlingStrategy,
|
||||
): opts is UrlHandlingStrategy {
|
||||
// This property check is needed because UrlHandlingStrategy is an interface and doesn't exist at
|
||||
// runtime.
|
||||
return 'shouldProcessUrl' in opts;
|
||||
|
|
@ -20,8 +37,9 @@ function isUrlHandlingStrategy(opts: ExtraOptions|
|
|||
|
||||
function throwInvalidConfigError(parameter: string): never {
|
||||
throw new Error(
|
||||
`Parameter ${parameter} does not match the one available in the injector. ` +
|
||||
'`setupTestingRouter` is meant to be used as a factory function with dependencies coming from DI.');
|
||||
`Parameter ${parameter} does not match the one available in the injector. ` +
|
||||
'`setupTestingRouter` is meant to be used as a factory function with dependencies coming from DI.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,17 +74,19 @@ function throwInvalidConfigError(parameter: string): never {
|
|||
provideLocationMocks(),
|
||||
withPreloading(NoPreloading).ɵproviders,
|
||||
{provide: ROUTES, multi: true, useValue: []},
|
||||
]
|
||||
],
|
||||
})
|
||||
export class RouterTestingModule {
|
||||
static withRoutes(routes: Routes, config?: ExtraOptions):
|
||||
ModuleWithProviders<RouterTestingModule> {
|
||||
static withRoutes(
|
||||
routes: Routes,
|
||||
config?: ExtraOptions,
|
||||
): ModuleWithProviders<RouterTestingModule> {
|
||||
return {
|
||||
ngModule: RouterTestingModule,
|
||||
providers: [
|
||||
{provide: ROUTES, multi: true, useValue: routes},
|
||||
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,20 @@ describe('navigateForTest', () => {
|
|||
it('executes guards on the path', async () => {
|
||||
let guardCalled = false;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{
|
||||
path: '',
|
||||
canActivate: [() => {
|
||||
guardCalled = true;
|
||||
return true;
|
||||
}],
|
||||
children: []
|
||||
}])]
|
||||
providers: [
|
||||
provideRouter([
|
||||
{
|
||||
path: '',
|
||||
canActivate: [
|
||||
() => {
|
||||
guardCalled = true;
|
||||
return true;
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('/');
|
||||
expect(guardCalled).toBeTrue();
|
||||
|
|
@ -54,17 +60,22 @@ describe('navigateForTest', () => {
|
|||
|
||||
it('throws error if routing throws', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'e',
|
||||
canActivate: [() => {
|
||||
throw new Error('oh no');
|
||||
}],
|
||||
children: []
|
||||
canActivate: [
|
||||
() => {
|
||||
throw new Error('oh no');
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
withRouterConfig({resolveNavigationPromiseOnError: true}))]
|
||||
withRouterConfig({resolveNavigationPromiseOnError: true}),
|
||||
),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await expectAsync(harness.navigateByUrl('e')).toBeResolvedTo(null);
|
||||
|
|
@ -77,9 +88,7 @@ describe('navigateForTest', () => {
|
|||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter([{path: ':id', component: TestCmp}]),
|
||||
]
|
||||
providers: [provideRouter([{path: ':id', component: TestCmp}])],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
const activatedComponent = await harness.navigateByUrl('/123', TestCmp);
|
||||
|
|
@ -89,33 +98,25 @@ describe('navigateForTest', () => {
|
|||
expect(harness.routeNativeElement?.innerHTML).toContain('456');
|
||||
});
|
||||
|
||||
it('throws an error if the routed component instance does not match the one required',
|
||||
async () => {
|
||||
@Component({standalone: true, template: ''})
|
||||
class TestCmp {
|
||||
}
|
||||
@Component({standalone: true, template: ''})
|
||||
class OtherCmp {
|
||||
}
|
||||
it('throws an error if the routed component instance does not match the one required', async () => {
|
||||
@Component({standalone: true, template: ''})
|
||||
class TestCmp {}
|
||||
@Component({standalone: true, template: ''})
|
||||
class OtherCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter([{path: '**', component: TestCmp}]),
|
||||
]
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await expectAsync(harness.navigateByUrl('/123', OtherCmp)).toBeRejected();
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([{path: '**', component: TestCmp}])],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await expectAsync(harness.navigateByUrl('/123', OtherCmp)).toBeRejected();
|
||||
});
|
||||
|
||||
it('throws an error if navigation fails but expected a component instance', async () => {
|
||||
@Component({standalone: true, template: ''})
|
||||
class TestCmp {
|
||||
}
|
||||
class TestCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter([{path: '**', canActivate: [() => false], component: TestCmp}]),
|
||||
]
|
||||
providers: [provideRouter([{path: '**', canActivate: [() => false], component: TestCmp}])],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await expectAsync(harness.navigateByUrl('/123', TestCmp)).toBeRejected();
|
||||
|
|
@ -123,11 +124,9 @@ describe('navigateForTest', () => {
|
|||
|
||||
it('waits for redirects using router.navigate', async () => {
|
||||
@Component({standalone: true, template: 'test'})
|
||||
class TestCmp {
|
||||
}
|
||||
class TestCmp {}
|
||||
@Component({standalone: true, template: 'redirect'})
|
||||
class OtherCmp {
|
||||
}
|
||||
class OtherCmp {}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
|
@ -135,11 +134,11 @@ describe('navigateForTest', () => {
|
|||
{
|
||||
path: 'test',
|
||||
canActivate: [() => inject(Router).navigateByUrl('/redirect')],
|
||||
component: TestCmp
|
||||
component: TestCmp,
|
||||
},
|
||||
{path: 'redirect', canActivate: [() => of(true).pipe(delay(100))], component: OtherCmp},
|
||||
]),
|
||||
]
|
||||
],
|
||||
});
|
||||
await RouterTestingHarness.create('test');
|
||||
expect(TestBed.inject(Router).url).toEqual('/redirect');
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const RouterUpgradeInitializer = {
|
|||
provide: APP_BOOTSTRAP_LISTENER,
|
||||
multi: true,
|
||||
useFactory: locationSyncBootstrapListener as (ngUpgrade: UpgradeModule) => () => void,
|
||||
deps: [UpgradeModule]
|
||||
deps: [UpgradeModule],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -62,7 +62,7 @@ export function locationSyncBootstrapListener(ngUpgrade: UpgradeModule) {
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'hash' = 'path') {
|
||||
export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path' | 'hash' = 'path') {
|
||||
if (!ngUpgrade.$injector) {
|
||||
throw new Error(`
|
||||
RouterUpgradeInitializer can be used only after UpgradeModule.bootstrap has been called.
|
||||
|
|
@ -73,36 +73,41 @@ export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'has
|
|||
const router: Router = ngUpgrade.injector.get(Router);
|
||||
const location: Location = ngUpgrade.injector.get(Location);
|
||||
|
||||
ngUpgrade.$injector.get('$rootScope')
|
||||
.$on(
|
||||
'$locationChangeStart',
|
||||
(event: any, newUrl: string, oldUrl: string,
|
||||
newState?: {[k: string]: unknown}|RestoredState,
|
||||
oldState?: {[k: string]: unknown}|RestoredState) => {
|
||||
// Navigations coming from Angular router have a navigationId state
|
||||
// property. Don't trigger Angular router navigation again if it is
|
||||
// caused by a URL change from the current Angular router
|
||||
// navigation.
|
||||
const currentNavigationId = router.getCurrentNavigation()?.id;
|
||||
const newStateNavigationId = newState?.navigationId;
|
||||
if (newStateNavigationId !== undefined &&
|
||||
newStateNavigationId === currentNavigationId) {
|
||||
return;
|
||||
}
|
||||
ngUpgrade.$injector
|
||||
.get('$rootScope')
|
||||
.$on(
|
||||
'$locationChangeStart',
|
||||
(
|
||||
event: any,
|
||||
newUrl: string,
|
||||
oldUrl: string,
|
||||
newState?: {[k: string]: unknown} | RestoredState,
|
||||
oldState?: {[k: string]: unknown} | RestoredState,
|
||||
) => {
|
||||
// Navigations coming from Angular router have a navigationId state
|
||||
// property. Don't trigger Angular router navigation again if it is
|
||||
// caused by a URL change from the current Angular router
|
||||
// navigation.
|
||||
const currentNavigationId = router.getCurrentNavigation()?.id;
|
||||
const newStateNavigationId = newState?.navigationId;
|
||||
if (newStateNavigationId !== undefined && newStateNavigationId === currentNavigationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url;
|
||||
if (urlType === 'path') {
|
||||
url = resolveUrl(newUrl);
|
||||
} else if (urlType === 'hash') {
|
||||
// Remove the first hash from the URL
|
||||
const hashIdx = newUrl.indexOf('#');
|
||||
url = resolveUrl(newUrl.substring(0, hashIdx) + newUrl.substring(hashIdx + 1));
|
||||
} else {
|
||||
throw 'Invalid URLType passed to setUpLocationSync: ' + urlType;
|
||||
}
|
||||
const path = location.normalize(url.pathname);
|
||||
router.navigateByUrl(path + url.search + url.hash);
|
||||
});
|
||||
let url;
|
||||
if (urlType === 'path') {
|
||||
url = resolveUrl(newUrl);
|
||||
} else if (urlType === 'hash') {
|
||||
// Remove the first hash from the URL
|
||||
const hashIdx = newUrl.indexOf('#');
|
||||
url = resolveUrl(newUrl.substring(0, hashIdx) + newUrl.substring(hashIdx + 1));
|
||||
} else {
|
||||
throw 'Invalid URLType passed to setUpLocationSync: ' + urlType;
|
||||
}
|
||||
const path = location.normalize(url.pathname);
|
||||
router.navigateByUrl(path + url.search + url.hash);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,8 +128,8 @@ export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'has
|
|||
* https://github.com/angular/angular.js/blob/2c7400e7d07b0f6cec1817dab40b9250ce8ebce6/src/ng/urlUtils.js#L26-L33
|
||||
* for more info.
|
||||
*/
|
||||
let anchor: HTMLAnchorElement|undefined;
|
||||
function resolveUrl(url: string): {pathname: string, search: string, hash: string} {
|
||||
let anchor: HTMLAnchorElement | undefined;
|
||||
function resolveUrl(url: string): {pathname: string; search: string; hash: string} {
|
||||
anchor ??= document.createElement('a');
|
||||
|
||||
anchor.setAttribute('href', url);
|
||||
|
|
@ -134,6 +139,6 @@ function resolveUrl(url: string): {pathname: string, search: string, hash: strin
|
|||
// IE does not start `pathname` with `/` like other browsers.
|
||||
pathname: `/${anchor.pathname.replace(/^\//, '')}`,
|
||||
search: anchor.search,
|
||||
hash: anchor.hash
|
||||
hash: anchor.hash,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class $rootScopeMock {
|
|||
|
||||
$broadcast(evt: string, ...args: any[]) {
|
||||
if (this.events[evt]) {
|
||||
this.events[evt].forEach(fn => {
|
||||
this.events[evt].forEach((fn) => {
|
||||
fn.apply(fn, [/** angular.IAngularEvent*/ {}, ...args]);
|
||||
});
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ export class $rootScopeMock {
|
|||
defaultPrevented: false,
|
||||
preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ export class $rootScopeMock {
|
|||
}
|
||||
|
||||
$digest() {
|
||||
this.watchers.forEach(fn => fn());
|
||||
this.watchers.forEach((fn) => fn());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,10 @@ describe('setUpLocationSync', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{path: '1', children: []}, {path: '2', children: []}]),
|
||||
RouterModule.forRoot([
|
||||
{path: '1', children: []},
|
||||
{path: '2', children: []},
|
||||
]),
|
||||
UpgradeModule,
|
||||
LocationUpgradeTestModule.config(),
|
||||
],
|
||||
|
|
@ -96,16 +99,15 @@ describe('setUpLocationSync', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('should get the $rootScope from AngularJS and set an $on watch on $locationChangeStart',
|
||||
() => {
|
||||
const $rootScope = upgradeModule.$injector.get('$rootScope');
|
||||
spyOn($rootScope, '$on');
|
||||
it('should get the $rootScope from AngularJS and set an $on watch on $locationChangeStart', () => {
|
||||
const $rootScope = upgradeModule.$injector.get('$rootScope');
|
||||
spyOn($rootScope, '$on');
|
||||
|
||||
setUpLocationSync(upgradeModule);
|
||||
setUpLocationSync(upgradeModule);
|
||||
|
||||
expect($rootScope.$on).toHaveBeenCalledTimes(1);
|
||||
expect($rootScope.$on).toHaveBeenCalledWith('$locationChangeStart', jasmine.any(Function));
|
||||
});
|
||||
expect($rootScope.$on).toHaveBeenCalledTimes(1);
|
||||
expect($rootScope.$on).toHaveBeenCalledWith('$locationChangeStart', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should navigate by url every time $locationChangeStart is broadcasted', () => {
|
||||
const url = 'https://google.com';
|
||||
|
|
@ -168,36 +170,36 @@ describe('setUpLocationSync', () => {
|
|||
});
|
||||
|
||||
it('should not duplicate navigations triggered by Angular router', fakeAsync(() => {
|
||||
spyOn(TestBed.inject(UrlCodec), 'parse').and.returnValue({
|
||||
pathname: '',
|
||||
href: '',
|
||||
protocol: '',
|
||||
host: '',
|
||||
search: '',
|
||||
hash: '',
|
||||
hostname: '',
|
||||
port: '',
|
||||
});
|
||||
const $rootScope = upgradeModule.$injector.get('$rootScope');
|
||||
spyOn($rootScope, '$broadcast').and.callThrough();
|
||||
setUpLocationSync(upgradeModule);
|
||||
// Inject location shim so its urlChangeListener subscribes
|
||||
TestBed.inject($locationShim);
|
||||
spyOn(TestBed.inject(UrlCodec), 'parse').and.returnValue({
|
||||
pathname: '',
|
||||
href: '',
|
||||
protocol: '',
|
||||
host: '',
|
||||
search: '',
|
||||
hash: '',
|
||||
hostname: '',
|
||||
port: '',
|
||||
});
|
||||
const $rootScope = upgradeModule.$injector.get('$rootScope');
|
||||
spyOn($rootScope, '$broadcast').and.callThrough();
|
||||
setUpLocationSync(upgradeModule);
|
||||
// Inject location shim so its urlChangeListener subscribes
|
||||
TestBed.inject($locationShim);
|
||||
|
||||
router.navigateByUrl('/1');
|
||||
location.normalize.and.returnValue('/1');
|
||||
flush();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart');
|
||||
expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess');
|
||||
$rootScope.$broadcast.calls.reset();
|
||||
router.navigateByUrl.calls.reset();
|
||||
router.navigateByUrl('/1');
|
||||
location.normalize.and.returnValue('/1');
|
||||
flush();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart');
|
||||
expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess');
|
||||
$rootScope.$broadcast.calls.reset();
|
||||
router.navigateByUrl.calls.reset();
|
||||
|
||||
location.go('/2');
|
||||
location.normalize.and.returnValue('/2');
|
||||
flush();
|
||||
expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart');
|
||||
expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess');
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
location.go('/2');
|
||||
location.normalize.and.returnValue('/2');
|
||||
flush();
|
||||
expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart');
|
||||
expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess');
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,9 +6,20 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {APP_BASE_HREF, CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common';
|
||||
import {
|
||||
APP_BASE_HREF,
|
||||
CommonModule,
|
||||
Location,
|
||||
LocationStrategy,
|
||||
PlatformLocation,
|
||||
} from '@angular/common';
|
||||
import {MockPlatformLocation} from '@angular/common/testing';
|
||||
import {$locationShim, $locationShimProvider, LocationUpgradeModule, UrlCodec} from '@angular/common/upgrade';
|
||||
import {
|
||||
$locationShim,
|
||||
$locationShimProvider,
|
||||
LocationUpgradeModule,
|
||||
UrlCodec,
|
||||
} from '@angular/common/upgrade';
|
||||
import {Inject, InjectionToken, ModuleWithProviders, NgModule, Optional} from '@angular/core';
|
||||
import {UpgradeModule} from '@angular/upgrade/static';
|
||||
|
||||
|
|
@ -25,9 +36,9 @@ export interface LocationUpgradeTestingConfig {
|
|||
*
|
||||
* Is used in DI to configure the router.
|
||||
*/
|
||||
export const LOC_UPGRADE_TEST_CONFIG =
|
||||
new InjectionToken<LocationUpgradeTestingConfig>('LOC_UPGRADE_TEST_CONFIG');
|
||||
|
||||
export const LOC_UPGRADE_TEST_CONFIG = new InjectionToken<LocationUpgradeTestingConfig>(
|
||||
'LOC_UPGRADE_TEST_CONFIG',
|
||||
);
|
||||
|
||||
export const APP_BASE_HREF_RESOLVED = new InjectionToken<string>('APP_BASE_HREF_RESOLVED');
|
||||
|
||||
|
|
@ -36,12 +47,14 @@ export const APP_BASE_HREF_RESOLVED = new InjectionToken<string>('APP_BASE_HREF_
|
|||
*/
|
||||
@NgModule({imports: [CommonModule]})
|
||||
export class LocationUpgradeTestModule {
|
||||
static config(config?: LocationUpgradeTestingConfig):
|
||||
ModuleWithProviders<LocationUpgradeTestModule> {
|
||||
static config(
|
||||
config?: LocationUpgradeTestingConfig,
|
||||
): ModuleWithProviders<LocationUpgradeTestModule> {
|
||||
return {
|
||||
ngModule: LocationUpgradeTestModule,
|
||||
providers: [
|
||||
{provide: LOC_UPGRADE_TEST_CONFIG, useValue: config || {}}, {
|
||||
{provide: LOC_UPGRADE_TEST_CONFIG, useValue: config || {}},
|
||||
{
|
||||
provide: PlatformLocation,
|
||||
useFactory: (appBaseHref?: string) => {
|
||||
if (config && config.appBaseHref != null) {
|
||||
|
|
@ -49,35 +62,49 @@ export class LocationUpgradeTestModule {
|
|||
} else if (appBaseHref == null) {
|
||||
appBaseHref = '';
|
||||
}
|
||||
return new MockPlatformLocation(
|
||||
{startUrl: config && config.startUrl, appBaseHref: appBaseHref});
|
||||
return new MockPlatformLocation({
|
||||
startUrl: config && config.startUrl,
|
||||
appBaseHref: appBaseHref,
|
||||
});
|
||||
},
|
||||
deps: [[new Inject(APP_BASE_HREF), new Optional()]]
|
||||
deps: [[new Inject(APP_BASE_HREF), new Optional()]],
|
||||
},
|
||||
{
|
||||
provide: $locationShim,
|
||||
useFactory: provide$location,
|
||||
deps: [
|
||||
UpgradeModule, Location, PlatformLocation, UrlCodec, LocationStrategy,
|
||||
LOC_UPGRADE_TEST_CONFIG
|
||||
]
|
||||
UpgradeModule,
|
||||
Location,
|
||||
PlatformLocation,
|
||||
UrlCodec,
|
||||
LocationStrategy,
|
||||
LOC_UPGRADE_TEST_CONFIG,
|
||||
],
|
||||
},
|
||||
LocationUpgradeModule
|
||||
.config({
|
||||
appBaseHref: config && config.appBaseHref,
|
||||
useHash: config && config.useHash || false
|
||||
})
|
||||
.providers!
|
||||
LocationUpgradeModule.config({
|
||||
appBaseHref: config && config.appBaseHref,
|
||||
useHash: (config && config.useHash) || false,
|
||||
}).providers!,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function provide$location(
|
||||
ngUpgrade: UpgradeModule, location: Location, platformLocation: PlatformLocation,
|
||||
urlCodec: UrlCodec, locationStrategy: LocationStrategy, config?: LocationUpgradeTestingConfig) {
|
||||
const $locationProvider =
|
||||
new $locationShimProvider(ngUpgrade, location, platformLocation, urlCodec, locationStrategy);
|
||||
ngUpgrade: UpgradeModule,
|
||||
location: Location,
|
||||
platformLocation: PlatformLocation,
|
||||
urlCodec: UrlCodec,
|
||||
locationStrategy: LocationStrategy,
|
||||
config?: LocationUpgradeTestingConfig,
|
||||
) {
|
||||
const $locationProvider = new $locationShimProvider(
|
||||
ngUpgrade,
|
||||
location,
|
||||
platformLocation,
|
||||
urlCodec,
|
||||
locationStrategy,
|
||||
);
|
||||
|
||||
$locationProvider.hashPrefix(config && config.hashPrefix);
|
||||
$locationProvider.html5Mode(config && !config.useHash);
|
||||
|
|
|
|||
Loading…
Reference in a new issue