Skip to content
This repository was archived by the owner on Feb 8, 2020. It is now read-only.

Commit 849d952

Browse files
committed
feat: make deep link handling more flexible
This adds ability to specify a custom config to control how to convert between state and path. Example: ```js { Chat: { path: 'chat/:author/:id', parse: { id: Number } } } ``` The above config can parse a path matching the provided pattern: `chat/jane/42` to a valid state: ```js { routes: [ { name: 'Chat', params: { author: 'jane', id: 42 }, }, ], } ``` This makes it much easier to control the parsing without having to specify a custom function.
1 parent 17045f5 commit 849d952

File tree

8 files changed

+341
-47
lines changed

8 files changed

+341
-47
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@types/jest": "^24.0.13",
3232
"codecov": "^3.5.0",
3333
"commitlint": "^8.0.0",
34-
"core-js": "^3.1.4",
34+
"core-js": "^3.2.1",
3535
"eslint": "^5.16.0",
3636
"eslint-config-satya164": "^2.4.1",
3737
"husky": "^2.4.0",

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"clean": "del lib"
3030
},
3131
"dependencies": {
32+
"escape-string-regexp": "^2.0.0",
33+
"query-string": "^6.8.3",
3234
"shortid": "^2.2.14",
3335
"use-subscription": "^1.0.0"
3436
},

packages/core/src/__tests__/getPathFromState.test.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import getPathFromState from '../getPathFromState';
22

3-
it('converts path string to initial state', () => {
3+
it('converts state to path string', () => {
44
expect(
55
getPathFromState({
66
routes: [
@@ -12,11 +12,12 @@ it('converts path string to initial state', () => {
1212
{ name: 'boo' },
1313
{
1414
name: 'bar',
15+
params: { fruit: 'apple' },
1516
state: {
1617
routes: [
1718
{
1819
name: 'baz qux',
19-
params: { author: 'jane & co', valid: true },
20+
params: { author: 'jane', valid: true },
2021
},
2122
],
2223
},
@@ -26,9 +27,50 @@ it('converts path string to initial state', () => {
2627
},
2728
],
2829
})
29-
).toMatchInlineSnapshot(
30-
`"/foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true"`
31-
);
30+
).toMatchInlineSnapshot(`"/foo/bar/baz%20qux?author=jane&valid=true"`);
31+
});
32+
33+
it('converts state to path string with config', () => {
34+
expect(
35+
getPathFromState(
36+
{
37+
routes: [
38+
{
39+
name: 'Foo',
40+
state: {
41+
index: 1,
42+
routes: [
43+
{ name: 'boo' },
44+
{
45+
name: 'Bar',
46+
params: { fruit: 'apple', type: 'sweet', avaliable: false },
47+
state: {
48+
routes: [
49+
{
50+
name: 'Baz',
51+
params: { author: 'Jane', valid: true, id: 10 },
52+
},
53+
],
54+
},
55+
},
56+
],
57+
},
58+
},
59+
],
60+
},
61+
{
62+
Foo: 'few',
63+
Bar: 'bar/:type/:fruit',
64+
Baz: {
65+
path: 'baz/:author',
66+
stringify: {
67+
author: author => author.toLowerCase(),
68+
id: id => `x${id}`,
69+
},
70+
},
71+
}
72+
)
73+
).toMatchInlineSnapshot(`"/few/bar/sweet/apple/baz/jane?id=x10&valid=true"`);
3274
});
3375

3476
it('handles route without param', () => {

packages/core/src/__tests__/getStateFromPath.test.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import getStateFromPath from '../getStateFromPath';
22

33
it('converts path string to initial state', () => {
44
expect(
5-
getStateFromPath(
6-
'foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true'
7-
)
5+
getStateFromPath('foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
86
).toEqual({
97
routes: [
108
{
@@ -17,7 +15,55 @@ it('converts path string to initial state', () => {
1715
routes: [
1816
{
1917
name: 'baz qux',
20-
params: { author: 'jane & co', valid: true },
18+
params: { author: 'jane & co', valid: 'true' },
19+
},
20+
],
21+
},
22+
},
23+
],
24+
},
25+
},
26+
],
27+
});
28+
});
29+
30+
it('converts path string to initial state with config', () => {
31+
expect(
32+
getStateFromPath(
33+
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
34+
{
35+
Foo: 'few',
36+
Bar: 'bar/:type/:fruit',
37+
Baz: {
38+
path: 'baz/:author',
39+
parse: {
40+
author: (author: string) =>
41+
author.replace(/^\w/, c => c.toUpperCase()),
42+
count: Number,
43+
valid: Boolean,
44+
},
45+
},
46+
}
47+
)
48+
).toEqual({
49+
routes: [
50+
{
51+
name: 'Foo',
52+
state: {
53+
routes: [
54+
{
55+
name: 'Bar',
56+
params: { fruit: 'apple', type: 'sweet' },
57+
state: {
58+
routes: [
59+
{
60+
name: 'Baz',
61+
params: {
62+
author: 'Jane',
63+
count: 10,
64+
answer: '42',
65+
valid: true,
66+
},
2167
},
2268
],
2369
},
@@ -38,7 +84,7 @@ it('handles leading slash when converting', () => {
3884
routes: [
3985
{
4086
name: 'bar',
41-
params: { count: 42 },
87+
params: { count: '42' },
4288
},
4389
],
4490
},
@@ -56,7 +102,7 @@ it('handles ending slash when converting', () => {
56102
routes: [
57103
{
58104
name: 'bar',
59-
params: { count: 42 },
105+
params: { count: '42' },
60106
},
61107
],
62108
},

packages/core/src/getPathFromState.tsx

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,45 @@
1+
import queryString from 'query-string';
12
import { NavigationState, PartialState, Route } from './types';
23

34
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
45

6+
type StringifyConfig = { [key: string]: (value: any) => string };
7+
8+
type Options = {
9+
[routeName: string]: string | { path: string; stringify?: StringifyConfig };
10+
};
11+
512
/**
613
* Utility to serialize a navigation state object to a path string.
714
*
15+
* Example:
16+
* ```js
17+
* getPathFromState(
18+
* {
19+
* routes: [
20+
* {
21+
* name: 'Chat',
22+
* params: { author: 'Jane', id: 42 },
23+
* },
24+
* ],
25+
* },
26+
* {
27+
* Chat: {
28+
* path: 'chat/:author/:id',
29+
* stringify: { author: author => author.toLowerCase() }
30+
* }
31+
* }
32+
* )
33+
* ```
34+
*
835
* @param state Navigation state to serialize.
36+
* @param options Extra options to fine-tune how to serialize the path.
937
* @returns Path representing the state, e.g. /foo/bar?count=42.
1038
*/
11-
export default function getPathFromState(state: State): string {
39+
export default function getPathFromState(
40+
state: State,
41+
options: Options = {}
42+
): string {
1243
let path = '/';
1344

1445
let current: State | undefined = state;
@@ -19,24 +50,51 @@ export default function getPathFromState(state: State): string {
1950
state?: State | undefined;
2051
};
2152

22-
path += encodeURIComponent(route.name);
53+
const config =
54+
options[route.name] !== undefined
55+
? (options[route.name] as { stringify?: StringifyConfig }).stringify
56+
: undefined;
2357

24-
if (route.state) {
25-
path += '/';
26-
} else if (route.params) {
27-
const query = [];
58+
const params = route.params
59+
? // Stringify all of the param values before we use them
60+
Object.entries(route.params).reduce<{
61+
[key: string]: string;
62+
}>((acc, [key, value]) => {
63+
acc[key] = config && config[key] ? config[key](value) : String(value);
64+
return acc;
65+
}, {})
66+
: undefined;
2867

29-
for (const param in route.params) {
30-
const value = (route.params as { [key: string]: any })[param];
68+
if (options[route.name] !== undefined) {
69+
const pattern =
70+
typeof options[route.name] === 'string'
71+
? (options[route.name] as string)
72+
: (options[route.name] as { path: string }).path;
3173

32-
query.push(
33-
`${encodeURIComponent(param)}=${encodeURIComponent(
34-
JSON.stringify(value)
35-
)}`
36-
);
37-
}
74+
path += pattern
75+
.split('/')
76+
.map(p => {
77+
const name = p.replace(/^:/, '');
3878

39-
path += `?${query.join('&')}`;
79+
// If the path has a pattern for a param, put the param in the path
80+
if (params && name in params && p.startsWith(':')) {
81+
const value = params[name];
82+
// Remove the used value from the params object since we'll use the rest for query string
83+
delete params[name];
84+
return encodeURIComponent(value);
85+
}
86+
87+
return encodeURIComponent(p);
88+
})
89+
.join('/');
90+
} else {
91+
path += encodeURIComponent(route.name);
92+
}
93+
94+
if (route.state) {
95+
path += '/';
96+
} else if (params) {
97+
path += `?${queryString.stringify(params)}`;
4098
}
4199

42100
current = route.state;

0 commit comments

Comments
 (0)