Skip to content

Commit

Permalink
refactor(router): Small refactor of createUrlTree and extra tests (#3…
Browse files Browse the repository at this point in the history
…9456)

This commit has a small refactor of some methods in create_url_tree.ts
and adds some test cases, including two that will fail at the moment but
should pass. A follow-up commit will make use of the refactorings to fix
the test with minimal changes.

PR Close #39456
  • Loading branch information
atscott authored and josephperrott committed Oct 30, 2020
1 parent f12157c commit ff7a62e
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 10 deletions.
6 changes: 3 additions & 3 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -1376,9 +1376,6 @@
{
"name": "getParentInjectorView"
},
{
"name": "getPath"
},
{
"name": "getPathIndexShift"
},
Expand Down Expand Up @@ -1490,6 +1487,9 @@
{
"name": "isArrayLike"
},
{
"name": "isCommandWithOutlets"
},
{
"name": "isComponentDef"
},
Expand Down
22 changes: 15 additions & 7 deletions packages/router/src/create_url_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ function isMatrixParams(command: any): boolean {
return typeof command === 'object' && command != null && !command.outlets && !command.segmentPath;
}

/**
* Determines if a given command has an `outlets` map. When we encounter a command
* with an outlets k/v map, we need to apply each outlet individually to the existing segment.
*/
function isCommandWithOutlets(command: any): command is {outlets: {[key: string]: any}} {
return typeof command === 'object' && command != null && command.outlets;
}

function tree(
oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, urlTree: UrlTree,
queryParams: Params, fragment: string): UrlTree {
Expand Down Expand Up @@ -75,7 +83,7 @@ class Navigation {
throw new Error('Root segment cannot have matrix parameters');
}

const cmdWithOutlet = commands.find(c => typeof c === 'object' && c != null && c.outlets);
const cmdWithOutlet = commands.find(isCommandWithOutlets);
if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
throw new Error('{outlets:{}} has to be the last command');
}
Expand Down Expand Up @@ -179,14 +187,14 @@ function createPositionApplyingDoubleDots(
}

function getPath(command: any): any {
if (typeof command === 'object' && command != null && command.outlets) {
if (isCommandWithOutlets(command)) {
return command.outlets[PRIMARY_OUTLET];
}
return `${command}`;
}

function getOutlets(commands: any[]): {[k: string]: any[]} {
if (typeof commands[0] === 'object' && commands[0] !== null && commands[0].outlets) {
if (isCommandWithOutlets(commands[0])) {
return commands[0].outlets;
}

Expand Down Expand Up @@ -276,9 +284,9 @@ function createNewSegmentGroup(

let i = 0;
while (i < commands.length) {
if (typeof commands[i] === 'object' && commands[i] !== null &&
commands[i].outlets !== undefined) {
const children = createNewSegmentChildren(commands[i].outlets);
const command = commands[i];
if (isCommandWithOutlets(command)) {
const children = createNewSegmentChildren(command.outlets);
return new UrlSegmentGroup(paths, children);
}

Expand All @@ -290,7 +298,7 @@ function createNewSegmentGroup(
continue;
}

const curr = getPath(commands[i]);
const curr = getPath(command);
const next = (i < commands.length - 1) ? commands[i + 1] : null;
if (curr && next && isMatrixParams(next)) {
paths.push(new UrlSegment(curr, stringify(next)));
Expand Down
94 changes: 94 additions & 0 deletions packages/router/test/create_url_tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,100 @@ describe('createUrlTree', () => {
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)');
});

describe('', () => {
/**
* In this group of scenarios, imagine a config like:
* {
* path: 'parent',
* children: [
* {
* path: 'child',
* component: AnyCmp
* },
* {
* path: 'popup',
* outlet: 'secondary',
* component: AnyCmp
* }
* ]
* },
* {
* path: 'other',
* component: AnyCmp
* },
* {
* path: 'rootPopup',
* outlet: 'rootSecondary',
* }
*/

it('should support removing secondary outlet with prefix', () => {
const p = serializer.parse('/parent/(child//secondary:popup)');
const t = createRoot(p, ['parent', {outlets: {secondary: null}}]);
// - Segment index 0:
// * match and keep existing 'parent'
// - Segment index 1:
// * 'secondary' outlet cleared with `null`
// * 'primary' outlet not provided in the commands list, so the existing value is kept
expect(serializer.serialize(t)).toEqual('/parent/child');
});

xit('should support updating secondary and primary outlets with prefix', () => {
const p = serializer.parse('/parent/child');
const t = createRoot(p, ['parent', {outlets: {primary: 'child', secondary: 'popup'}}]);
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
});

xit('should support updating two outlets at the same time relative to non-root segment', () => {
const p = serializer.parse('/parent/child');
const t = create(
p.root.children[PRIMARY_OUTLET], 0 /* relativeTo: 'parent' */, p,
[{outlets: {primary: 'child', secondary: 'popup'}}]);
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
});

it('should support adding multiple outlets with prefix', () => {
const p = serializer.parse('');
const t = createRoot(p, ['parent', {outlets: {primary: 'child', secondary: 'popup'}}]);
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
});

it('should support updating clearing primary and secondary with prefix', () => {
const p = serializer.parse('/parent/(child//secondary:popup)');
const t = createRoot(p, ['other']);
// Because we navigate away from the 'parent' route, the children of that route are cleared
// because they are note valid for the 'other' path.
expect(serializer.serialize(t)).toEqual('/other');
});

it('should not clear secondary outlet when at root and prefix is used', () => {
const p = serializer.parse('/other(rootSecondary:rootPopup)');
const t = createRoot(p, ['parent', {outlets: {primary: 'child', rootSecondary: null}}]);
// We prefixed the navigation with 'parent' so we cannot clear the "rootSecondary" outlet
// because once the outlets object is consumed, traversal is beyond the root segment.
expect(serializer.serialize(t)).toEqual('/parent/child(rootSecondary:rootPopup)');
});

it('should not clear non-root secondary outlet when command is targeting root', () => {
const p = serializer.parse('/parent/(child//secondary:popup)');
const t = createRoot(p, [{outlets: {secondary: null}}]);
// The start segment index for the command is at 0, but the outlet lives at index 1
// so we cannot clear the outlet from processing segment index 0.
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
});

it('can clear an auxiliary outlet at the correct segment level', () => {
const p = serializer.parse('/parent/(child//secondary:popup)(rootSecondary:rootPopup)');
// ^^^^^^^^^^^^^^^^^^^^^^
// The parens here show that 'child' and 'secondary:popup' appear at the same 'level' in the
// config, i.e. are part of the same children list. You can also imagine an implicit paren
// group around the whole URL to visualize how 'parent' and 'rootSecondary:rootPopup' are also
// defined at the same level.
const t = createRoot(p, ['parent', {outlets: {primary: 'child', secondary: null}}]);
expect(serializer.serialize(t)).toEqual('/parent/child(rootSecondary:rootPopup)');
});
});

it('should throw when outlets is not the last command', () => {
const p = serializer.parse('/a');
expect(() => createRoot(p, ['a', {outlets: {right: ['c']}}, 'c']))
Expand Down

0 comments on commit ff7a62e

Please sign in to comment.