refactor: migrate router to prettier formatting (#54318)

Migrate formatting to prettier for router from clang-format

PR Close #54318
This commit is contained in:
Joey Perrott 2024-02-07 16:00:01 +00:00 committed by Jessica Janiuk
parent d02fcb1ab4
commit b857aafcb9
79 changed files with 13550 additions and 10967 deletions

View file

@ -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}',
],

View file

@ -2,6 +2,7 @@
"printWidth": 100,
"tabWidth": 2,
"tabs": false,
"embeddedLanguageFormatting": "off",
"singleQuote": true,
"semicolon": true,
"quoteProps": "preserve",

View file

@ -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;
}

View file

@ -24,7 +24,6 @@ import {RouterOutlet} from '../directives/router_outlet';
imports: [RouterOutlet],
standalone: true,
})
export class ɵEmptyOutletComponent {
}
export class ɵEmptyOutletComponent {}
export {ɵEmptyOutletComponent as EmptyOutletComponent};

View file

@ -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,
);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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}')`;
}
}

View file

@ -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';

View file

@ -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

View file

@ -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';

View file

@ -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

View file

@ -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;
}
}

View file

@ -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));
}

View file

@ -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),
);
});
}

View file

@ -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};
}),
),
);
}

View file

@ -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);
}

View file

@ -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));

View file

@ -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;
}

View file

@ -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';

View file

@ -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.

View file

@ -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;
});

View file

@ -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 {}

View file

@ -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);
}

View file

@ -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: () => ({}),
},
);

View file

@ -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;

View file

@ -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 [

View file

@ -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;
}
}

View file

@ -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());

View file

@ -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);
});

View file

@ -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) {

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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];
}

View file

@ -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`

View file

@ -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();
});
}

View file

@ -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;

View file

@ -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;

View file

@ -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';
/**

View file

@ -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

View file

@ -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};

View file

@ -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 {}

View file

@ -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', () => {

View file

@ -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 {

View file

@ -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>) {

View file

@ -13,5 +13,4 @@ import {Component} from '@angular/core';
template: 'default exported',
selector: 'test-route',
})
export default class TestRoute {
}
export default class TestRoute {}

View file

@ -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;

View file

@ -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');

View file

@ -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

View file

@ -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);
}

View file

@ -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;

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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;
},
});
}

View file

@ -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

View file

@ -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};

View file

@ -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,
);
}

View file

@ -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');
});
});

View file

@ -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>) {

View file

@ -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);
}

View file

@ -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);
},
);
});
});
});

View file

@ -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([]);
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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 : {}},
]
],
};
}
}

View file

@ -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');

View file

@ -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,
};
}

View file

@ -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);
}));
});

View file

@ -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);