Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat(router): Add ability to create UrlTree from any `ActivatedRout…
…eSnapshot`

This exposes a new function from the router public API that allows
developers to create a `UrlTree` from _any_ `ActivatedRouteSnapshot`.
The current Router APIs only support creating a `UrlTree` from an
`ActivatedRoute` which is both active _and_ materially appears in the
`UrlTree` (it cannot be an empty path named outlet). This is because the
implementation of the current way of creating a `UrlTree` attempts to
look up the `UrlSegment` of the `ActivatedRoute` in the currently active
`UrlTree` of the router. When this doesn't work, the `UrlTree` creation
fails.

Note that this API does not replace the current one. That would actually be a
breaking change but should be done at some point in the future (v15). That is,
`router.navigate` should call this new function. At that point, we can
remove `_lastPathIndex`, `_urlSegment`, and `_correctedPathIndex` from
the `ActivatedRoute`, along with all of the logic associated with
determining what those should be. In addition, this would unblock a fix
for #26081 because the `applyRedirects` and `recognize` operations
could be combined into one.  Overall, this would simplify logic in the router
and reduce code size. It also exposes core routing capabilities as a helper function
which were previously private API, which is a necessary step towards #42953.

As a stress test for this new function, it _was_ swapped in as the
default for `UrlTree` creation in #45859 and tested internally. The
results indicate that this function behaves correctly.

resolves #42191 (Tested directly)
resolves #38276 (The test with a guard covers this case)
resolves #22763 (Tested directly)
  • Loading branch information
atscott committed May 17, 2022
commit 37403d98e13dd95305547b557a8297f1eb7d1deb
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);
}
102 changes: 98 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,77 @@
* 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.
*/
export function createUrlTreeFromSnapshot(
relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams: Params|null,
Comment thread
atscott marked this conversation as resolved.
Outdated
fragment: string|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;
}
const targetGroup = relativeTo;
Comment thread
atscott marked this conversation as resolved.
Outdated
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, targetGroup);
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 +142,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 +248,24 @@ class Position {
}
}

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

if (!target) {
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