mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-05-25 12:05:52 +08:00
feat: add support for optional params to linking (#8196)
This commit is contained in:
@@ -1192,3 +1192,588 @@ it('handles differently ordered params v4', () => {
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles simple optional params', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v1', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd?',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v2', () => {
|
||||
const path = '/foos/5/10';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd?',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5, nip: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle 2 optional params at the end v3', () => {
|
||||
const path = '/foos/5/10/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd?',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5, nip: 10, pwd: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v1', () => {
|
||||
const path = '/foos/5/10';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5, pwd: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v2', () => {
|
||||
const path = '/foos/5/10/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5, nip: 10, pwd: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v3', () => {
|
||||
const path = '/foos/5/10/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip?/:pwd/:smh',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
smh: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { id: 5, pwd: 10, smh: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v4', () => {
|
||||
const path = '/foos/5/10';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:nip?/:pwd/:smh?/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
smh: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { pwd: 5, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the middle v5', () => {
|
||||
const path = '/foos/5/10/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:nip?/:pwd/:smh?/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
smh: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { nip: 5, pwd: 10, id: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the beginning v1', () => {
|
||||
const path = '5/10/foos/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: ':nip?/:pwd/foos/:smh?/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
smh: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { nip: 5, pwd: 10, id: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handle optional params in the beginning v2', () => {
|
||||
const path = '5/10/foos/15';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: ':nip?/:smh?/:pwd/foos/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
nip: Number,
|
||||
pwd: Number,
|
||||
smh: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bas',
|
||||
params: { nip: 5, pwd: 10, id: 15 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function getPathFromState(
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map((p) => {
|
||||
const name = p.replace(/^:/, '');
|
||||
const name = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
|
||||
// If the path has a pattern for a param, put the param in the path
|
||||
if (params && name in params && p.startsWith(':')) {
|
||||
@@ -144,8 +144,10 @@ export default function getPathFromState(
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[name];
|
||||
return encodeURIComponent(value);
|
||||
} else if (p.endsWith('?')) {
|
||||
// optional params without value assigned in route.params should be ignored
|
||||
return '';
|
||||
}
|
||||
|
||||
return encodeURIComponent(p);
|
||||
})
|
||||
.join('/');
|
||||
@@ -173,7 +175,7 @@ export default function getPathFromState(
|
||||
}
|
||||
|
||||
// Remove multiple as well as trailing slashes
|
||||
path = path.replace(/\/+/, '/');
|
||||
path = path.replace(/\/+/g, '/');
|
||||
path = path.length > 1 ? path.replace(/\/$/, '') : path;
|
||||
|
||||
return path;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import escape from 'escape-string-regexp';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
NavigationState,
|
||||
@@ -20,6 +21,7 @@ type Options = {
|
||||
|
||||
type RouteConfig = {
|
||||
screen: string;
|
||||
match: RegExp | null;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: ParseConfig | undefined;
|
||||
@@ -73,11 +75,14 @@ export default function getStateFromPath(
|
||||
);
|
||||
|
||||
let remaining = path
|
||||
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/^\//, '') // Remove extra leading slash
|
||||
.replace(/\?.*/, ''); // Remove query params which we will handle later
|
||||
.replace(/\?.*$/, ''); // Remove query params which we will handle later
|
||||
|
||||
if (remaining === '') {
|
||||
// Make sure there is a trailing slash
|
||||
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
||||
|
||||
if (remaining === '/') {
|
||||
// We need to add special handling of empty path so navigation to empty path also works
|
||||
// When handling empty path, we should only look at the root level config
|
||||
const match = configs.find(
|
||||
@@ -109,48 +114,14 @@ export default function getStateFromPath(
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
for (const config of configs) {
|
||||
if (config.pattern === '') {
|
||||
if (!config.match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we check if remaining path has enough segments to be handled with this pattern
|
||||
if (config.pattern.split('/').length > remaining.split('/').length) {
|
||||
continue;
|
||||
}
|
||||
const match = remaining.match(config.match);
|
||||
|
||||
let patternParts = config.pattern.split('/');
|
||||
let remainingParts = remaining.split('/', patternParts.length);
|
||||
|
||||
// array of indexes of params to remove from path
|
||||
const paramIndexes = [] as number[];
|
||||
|
||||
// we extract params from the path
|
||||
const remainingParams = [] as string[];
|
||||
patternParts.forEach((p, index) => {
|
||||
if (p.startsWith(':')) {
|
||||
remainingParams.push(remainingParts[index]);
|
||||
paramIndexes.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
// filter the params from both arrays
|
||||
remainingParts = remainingParts.filter(
|
||||
(_, index) => !paramIndexes.includes(index)
|
||||
);
|
||||
patternParts = patternParts.filter(
|
||||
(_, index) => !paramIndexes.includes(index)
|
||||
);
|
||||
|
||||
let didMatch = true;
|
||||
// the non-param part of the remaining path should be the same as the non-param part of the config
|
||||
for (let i = 0; i < patternParts.length; i++) {
|
||||
if (patternParts[i] !== remainingParts[i]) {
|
||||
didMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (didMatch) {
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeNames = [...config.routeNames];
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
@@ -159,27 +130,21 @@ export default function getStateFromPath(
|
||||
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||
const key = p.replace(/^:/, '');
|
||||
const value = remainingParams[i];
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
: value;
|
||||
const key = p.replace(/^:/, '').replace(/\?$/, '');
|
||||
const value = match[(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
|
||||
|
||||
if (value) {
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
: value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// if pattern and remaining path have same amount of segments, there should be nothing left
|
||||
if (config.pattern.split('/').length === remaining.split('/').length) {
|
||||
remaining = '';
|
||||
} else {
|
||||
// For each segment of the pattern, remove one segment from remaining path
|
||||
let i = config.pattern.split('/').length;
|
||||
while (i--) {
|
||||
remaining = remaining.substr(remaining.indexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
remaining = remaining.replace(match[1], '');
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -283,8 +248,24 @@ function createConfigItem(
|
||||
pattern: string,
|
||||
parse?: ParseConfig
|
||||
): RouteConfig {
|
||||
const match = pattern
|
||||
? new RegExp(
|
||||
`^(${pattern
|
||||
.split('/')
|
||||
.map((it) => {
|
||||
if (it.startsWith(':')) {
|
||||
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
|
||||
}
|
||||
|
||||
return `${escape(it)}\\/`;
|
||||
})
|
||||
.join('')})`
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
screen,
|
||||
match,
|
||||
pattern,
|
||||
// The routeNames array is mutated, so copy it to keep the current state
|
||||
routeNames: [...routeNames],
|
||||
|
||||
Reference in New Issue
Block a user