Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@
],
[
"packages/router/src/models.ts",
"packages/router/src/router_state.ts",
"packages/router/src/url_tree.ts",
"packages/router/src/shared.ts"
],
[
Expand All @@ -341,10 +341,5 @@
[
"packages/router/src/shared.ts",
"packages/router/src/url_tree.ts"
],
[
"packages/router/src/shared.ts",
"packages/router/src/url_tree.ts",
"packages/router/src/utils/collection.ts"
]
]
3 changes: 3 additions & 0 deletions goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export class ChildrenOutletContexts {
// @public
export function convertToParamMap(params: Params): ParamMap;

// @public
export function createUrlTreeFromSnapshot(relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams?: Params | null, fragment?: string | null): UrlTree;

// @public
export type Data = {
[key: string | symbol]: any;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,9 @@
{
"name": "createResultForNode"
},
{
"name": "createRoot"
},
{
"name": "createRouterScroller"
},
Expand Down
42 changes: 2 additions & 40 deletions packages/router/src/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {CanLoadFn, LoadedRouterConfig, Route, Routes} from './models';
import {prioritizedGuardValue} from './operators/prioritized_guard_value';
import {RouterConfigLoader} from './router_config_loader';
import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {forEach, wrapIntoObservable} from './utils/collection';
import {getOrCreateRouteInjectorIfNeeded, getOutlet, sortByMatchingOutlets} from './utils/config';
import {isImmediateMatch, match, noLeftoversInUrl, split} from './utils/config_matching';
Expand Down Expand Up @@ -124,9 +124,7 @@ class ApplyRedirects {

private createUrlTree(rootCandidate: UrlSegmentGroup, queryParams: Params, fragment: string|null):
UrlTree {
const root = rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate;
const root = createRoot(rootCandidate);
return new UrlTree(root, queryParams, fragment);
}

Expand Down Expand Up @@ -475,39 +473,3 @@ class ApplyRedirects {
return redirectToUrlSegment;
}
}

/**
* When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
*
* When a segment group has only one child which is a primary outlet, merges that child into the
* parent. That is, the child segment group's segments are merged into the `s` and the child's
* children become the children of `s`. Think of this like a 'squash', merging the child segment
* group into the parent.
*/
function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
const c = s.children[PRIMARY_OUTLET];
return new UrlSegmentGroup(s.segments.concat(c.segments), c.children);
}

return s;
}

/**
* Recursively merges primary segment children into their parents and also drops empty children
* (those which have no segments and no children themselves). The latter prevents serializing a
* group into something like `/a(aux:)`, where `aux` is an empty child segment.
*/
function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
const newChildren = {} as any;
for (const childOutlet of Object.keys(segmentGroup.children)) {
const child = segmentGroup.children[childOutlet];
const childCandidate = squashSegmentGroup(child);
// don't add empty children
if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) {
newChildren[childOutlet] = childCandidate;
}
}
const s = new UrlSegmentGroup(segmentGroup.segments, newChildren);
return mergeTrivialChildren(s);
}
146 changes: 142 additions & 4 deletions packages/router/src/create_url_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,117 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ActivatedRoute} from './router_state';
import {ActivatedRoute, ActivatedRouteSnapshot} from './router_state';
import {Params, PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {forEach, last, shallowEqual} from './utils/collection';

/**
* Creates a `UrlTree` relative to an `ActivatedRouteSnapshot`.
*
* @publicApi
Comment thread
AndrewKushnir marked this conversation as resolved.
*
*
* @param relativeTo The `ActivatedRouteSnapshot` to apply the commands to
* @param commands An array of URL fragments with which to construct the new URL tree.
* If the path is static, can be the literal URL string. For a dynamic path, pass an array of path
* segments, followed by the parameters for each segment.
* The fragments are applied to the one provided in the `relativeTo` parameter.
Comment thread
atscott marked this conversation as resolved.
* @param queryParams The query parameters for the `UrlTree`. `null` if the `UrlTree` does not have
* any query parameters.
* @param fragment The fragment for the `UrlTree`. `null` if the `UrlTree` does not have a fragment.
*
* @usageNotes
*
* ```
* // create /team/33/user/11
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, 'user', 11]);
*
* // create /team/33;expand=true/user/11
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {expand: true}, 'user', 11]);
*
* // you can collapse static segments like this (this works only with the first passed-in value):
* createUrlTreeFromSnapshot(snapshot, ['/team/33/user', userId]);
*
* // If the first segment can contain slashes, and you do not want the router to split it,
* // you can do the following:
* createUrlTreeFromSnapshot(snapshot, [{segmentPath: '/one/two'}]);
*
* // create /team/33/(user/11//right:chat)
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {outlets: {primary: 'user/11', right:
* 'chat'}}], null, null);
*
* // remove the right secondary node
* createUrlTreeFromSnapshot(snapshot, ['/team', 33, {outlets: {primary: 'user/11', right: null}}]);
*
* // For the examples below, assume the current URL is for the `/team/33/user/11` and the
* `ActivatedRouteSnapshot` points to `user/11`:
*
* // navigate to /team/33/user/11/details
* createUrlTreeFromSnapshot(snapshot, ['details']);
*
* // navigate to /team/33/user/22
* createUrlTreeFromSnapshot(snapshot, ['../22']);
*
* // navigate to /team/44/user/22
* createUrlTreeFromSnapshot(snapshot, ['../../team/44/user/22']);
* ```
*/
export function createUrlTreeFromSnapshot(
relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams: Params|null = null,
fragment: string|null = null): UrlTree {
const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo);
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment);
}

function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup {
let targetGroup: UrlSegmentGroup|undefined;

function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot) {
const childOutlets: {[outlet: string]: UrlSegmentGroup} = {};
for (const childSnapshot of currentRoute.children) {
const root = createSegmentGroupFromRouteRecursive(childSnapshot);
childOutlets[childSnapshot.outlet] = root;
}
const segmentGroup = new UrlSegmentGroup(currentRoute.url, childOutlets);
if (currentRoute === route) {
targetGroup = segmentGroup;
}
return segmentGroup;
}
const rootCandidate = createSegmentGroupFromRouteRecursive(route.root);
const rootSegmentGroup = createRoot(rootCandidate);

return targetGroup ?? rootSegmentGroup;
}

export function createUrlTreeFromSegmentGroup(
relativeTo: UrlSegmentGroup, commands: any[], queryParams: Params|null,
fragment: string|null): UrlTree {
let root = relativeTo;
while (root.parent) {
root = root.parent;
}
// There are no commands so the `UrlTree` goes to the same path as the one created from the
// `UrlSegmentGroup`. All we need to do is update the `queryParams` and `fragment` without
// applying any other logic.
if (commands.length === 0) {
return tree(root, root, root, queryParams, fragment);
Comment thread
atscott marked this conversation as resolved.
}

const nav = computeNavigation(commands);

if (nav.toRoot()) {
return tree(root, root, new UrlSegmentGroup([], {}), queryParams, fragment);
}

const position = findStartingPositionForTargetGroup(nav, root, relativeTo);
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);
}

export function createUrlTree(
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params|null,
fragment: string|null): UrlTree {
Expand Down Expand Up @@ -76,14 +182,24 @@ function tree(
});
}

let rootCandidate: UrlSegmentGroup;
if (oldRoot === oldSegmentGroup) {
return new UrlTree(newSegmentGroup, qp, fragment);
rootCandidate = newSegmentGroup;
} else {
rootCandidate = replaceSegment(oldRoot, oldSegmentGroup, newSegmentGroup);
}

const newRoot = replaceSegment(oldRoot, oldSegmentGroup, newSegmentGroup);
const newRoot = createRoot(squashSegmentGroup(rootCandidate));
return new UrlTree(newRoot, qp, fragment);
}

/**
* Replaces the `oldSegment` which is located in some child of the `current` with the `newSegment`.
* This also has the effect of creating new `UrlSegmentGroup` copies to update references. This
* shouldn't be necessary but the fallback logic for an invalid ActivatedRoute in the creation uses
* the Router's current url tree. If we don't create new segment groups, we end up modifying that
* value.
*/
function replaceSegment(
current: UrlSegmentGroup, oldSegment: UrlSegmentGroup,
newSegment: UrlSegmentGroup): UrlSegmentGroup {
Expand Down Expand Up @@ -172,6 +288,28 @@ class Position {
}
}

function findStartingPositionForTargetGroup(
nav: Navigation, root: UrlSegmentGroup, target: UrlSegmentGroup): Position {
if (nav.isAbsolute) {
return new Position(root, true, 0);
}

if (!target) {
// `NaN` is used only to maintain backwards compatibility with incorrectly mocked
// `ActivatedRouteSnapshot` in tests. In prior versions of this code, the position here was
// determined based on an internal property that was rarely mocked, resulting in `NaN`. In
// reality, this code path should _never_ be touched since `target` is not allowed to be falsey.
return new Position(root, false, NaN);
Comment thread
atscott marked this conversation as resolved.
}
if (target.parent === null) {
return new Position(target, true, 0);
}

const modifier = isMatrixParams(nav.commands[0]) ? 0 : 1;
const index = target.segments.length - 1 + modifier;
return createPositionApplyingDoubleDots(target, index, nav.numberOfDoubleDots);
}

function findStartingPosition(
nav: Navigation, tree: UrlTree, segmentGroup: UrlSegmentGroup,
lastPathIndex: number): Position {
Expand Down
1 change: 1 addition & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/


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';
Expand Down
42 changes: 42 additions & 0 deletions packages/router/src/url_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,3 +728,45 @@ class UrlParser {
}
}
}

export function createRoot(rootCandidate: UrlSegmentGroup) {
return rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate;
}

/**
* Recursively merges primary segment children into their parents and also drops empty children
* (those which have no segments and no children themselves). The latter prevents serializing a
* group into something like `/a(aux:)`, where `aux` is an empty child segment.
*/
export function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
const newChildren = {} as any;
for (const childOutlet of Object.keys(segmentGroup.children)) {
const child = segmentGroup.children[childOutlet];
const childCandidate = squashSegmentGroup(child);
// don't add empty children
if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) {
newChildren[childOutlet] = childCandidate;
}
}
const s = new UrlSegmentGroup(segmentGroup.segments, newChildren);
return mergeTrivialChildren(s);
}

/**
* When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
*
* When a segment group has only one child which is a primary outlet, merges that child into the
* parent. That is, the child segment group's segments are merged into the `s` and the child's
* children become the children of `s`. Think of this like a 'squash', merging the child segment
* group into the parent.
*/
function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
const c = s.children[PRIMARY_OUTLET];
return new UrlSegmentGroup(s.segments.concat(c.segments), c.children);
}

return s;
}
Loading