refactor(router): Small refactor of createUrlTree and extra tests (#39456)

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
This commit is contained in:
Andrew Scott 2020-10-26 19:10:16 -07:00 committed by Joey Perrott
parent f12157c145
commit ff7a62ee21
3 changed files with 112 additions and 10 deletions

View File

@ -1376,9 +1376,6 @@
{ {
"name": "getParentInjectorView" "name": "getParentInjectorView"
}, },
{
"name": "getPath"
},
{ {
"name": "getPathIndexShift" "name": "getPathIndexShift"
}, },
@ -1490,6 +1487,9 @@
{ {
"name": "isArrayLike" "name": "isArrayLike"
}, },
{
"name": "isCommandWithOutlets"
},
{ {
"name": "isComponentDef" "name": "isComponentDef"
}, },

View File

@ -37,6 +37,14 @@ function isMatrixParams(command: any): boolean {
return typeof command === 'object' && command != null && !command.outlets && !command.segmentPath; 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( function tree(
oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, urlTree: UrlTree, oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, urlTree: UrlTree,
queryParams: Params, fragment: string): UrlTree { queryParams: Params, fragment: string): UrlTree {
@ -75,7 +83,7 @@ class Navigation {
throw new Error('Root segment cannot have matrix parameters'); 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)) { if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
throw new Error('{outlets:{}} has to be the last command'); throw new Error('{outlets:{}} has to be the last command');
} }
@ -179,14 +187,14 @@ function createPositionApplyingDoubleDots(
} }
function getPath(command: any): any { function getPath(command: any): any {
if (typeof command === 'object' && command != null && command.outlets) { if (isCommandWithOutlets(command)) {
return command.outlets[PRIMARY_OUTLET]; return command.outlets[PRIMARY_OUTLET];
} }
return `${command}`; return `${command}`;
} }
function getOutlets(commands: any[]): {[k: string]: any[]} { 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; return commands[0].outlets;
} }
@ -276,9 +284,9 @@ function createNewSegmentGroup(
let i = 0; let i = 0;
while (i < commands.length) { while (i < commands.length) {
if (typeof commands[i] === 'object' && commands[i] !== null && const command = commands[i];
commands[i].outlets !== undefined) { if (isCommandWithOutlets(command)) {
const children = createNewSegmentChildren(commands[i].outlets); const children = createNewSegmentChildren(command.outlets);
return new UrlSegmentGroup(paths, children); return new UrlSegmentGroup(paths, children);
} }
@ -290,7 +298,7 @@ function createNewSegmentGroup(
continue; continue;
} }
const curr = getPath(commands[i]); const curr = getPath(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)) { if (curr && next && isMatrixParams(next)) {
paths.push(new UrlSegment(curr, stringify(next))); paths.push(new UrlSegment(curr, stringify(next)));

View File

@ -112,6 +112,100 @@ describe('createUrlTree', () => {
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)'); 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', () => { it('should throw when outlets is not the last command', () => {
const p = serializer.parse('/a'); const p = serializer.parse('/a');
expect(() => createRoot(p, ['a', {outlets: {right: ['c']}}, 'c'])) expect(() => createRoot(p, ['a', {outlets: {right: ['c']}}, 'c']))