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:
parent
f12157c145
commit
ff7a62ee21
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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']))
|
||||||
|
|
Loading…
Reference in New Issue