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

Commit

Permalink
feat: make deep link handling more flexible
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
satya164 committed Sep 16, 2019
1 parent 17045f5 commit 849d952
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 47 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@types/jest": "^24.0.13",
"codecov": "^3.5.0",
"commitlint": "^8.0.0",
"core-js": "^3.1.4",
"core-js": "^3.2.1",
"eslint": "^5.16.0",
"eslint-config-satya164": "^2.4.1",
"husky": "^2.4.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"clean": "del lib"
},
"dependencies": {
"escape-string-regexp": "^2.0.0",
"query-string": "^6.8.3",
"shortid": "^2.2.14",
"use-subscription": "^1.0.0"
},
Expand Down
52 changes: 47 additions & 5 deletions packages/core/src/__tests__/getPathFromState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import getPathFromState from '../getPathFromState';

it('converts path string to initial state', () => {
it('converts state to path string', () => {
expect(
getPathFromState({
routes: [
Expand All @@ -12,11 +12,12 @@ it('converts path string to initial state', () => {
{ name: 'boo' },
{
name: 'bar',
params: { fruit: 'apple' },
state: {
routes: [
{
name: 'baz qux',
params: { author: 'jane & co', valid: true },
params: { author: 'jane', valid: true },
},
],
},
Expand All @@ -26,9 +27,50 @@ it('converts path string to initial state', () => {
},
],
})
).toMatchInlineSnapshot(
`"/foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true"`
);
).toMatchInlineSnapshot(`"/foo/bar/baz%20qux?author=jane&valid=true"`);
});

it('converts state to path string with config', () => {
expect(
getPathFromState(
{
routes: [
{
name: 'Foo',
state: {
index: 1,
routes: [
{ name: 'boo' },
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet', avaliable: false },
state: {
routes: [
{
name: 'Baz',
params: { author: 'Jane', valid: true, id: 10 },
},
],
},
},
],
},
},
],
},
{
Foo: 'few',
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
stringify: {
author: author => author.toLowerCase(),
id: id => `x${id}`,
},
},
}
)
).toMatchInlineSnapshot(`"/few/bar/sweet/apple/baz/jane?id=x10&valid=true"`);
});

it('handles route without param', () => {
Expand Down
58 changes: 52 additions & 6 deletions packages/core/src/__tests__/getStateFromPath.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import getStateFromPath from '../getStateFromPath';

it('converts path string to initial state', () => {
expect(
getStateFromPath(
'foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true'
)
getStateFromPath('foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
).toEqual({
routes: [
{
Expand All @@ -17,7 +15,55 @@ it('converts path string to initial state', () => {
routes: [
{
name: 'baz qux',
params: { author: 'jane & co', valid: true },
params: { author: 'jane & co', valid: 'true' },
},
],
},
},
],
},
},
],
});
});

it('converts path string to initial state with config', () => {
expect(
getStateFromPath(
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
{
Foo: 'few',
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, c => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
}
)
).toEqual({
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
Expand All @@ -38,7 +84,7 @@ it('handles leading slash when converting', () => {
routes: [
{
name: 'bar',
params: { count: 42 },
params: { count: '42' },
},
],
},
Expand All @@ -56,7 +102,7 @@ it('handles ending slash when converting', () => {
routes: [
{
name: 'bar',
params: { count: 42 },
params: { count: '42' },
},
],
},
Expand Down
88 changes: 73 additions & 15 deletions packages/core/src/getPathFromState.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import queryString from 'query-string';
import { NavigationState, PartialState, Route } from './types';

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

type StringifyConfig = { [key: string]: (value: any) => string };

type Options = {
[routeName: string]: string | { path: string; stringify?: StringifyConfig };
};

/**
* Utility to serialize a navigation state object to a path string.
*
* Example:
* ```js
* getPathFromState(
* {
* routes: [
* {
* name: 'Chat',
* params: { author: 'Jane', id: 42 },
* },
* ],
* },
* {
* Chat: {
* path: 'chat/:author/:id',
* stringify: { author: author => author.toLowerCase() }
* }
* }
* )
* ```
*
* @param state Navigation state to serialize.
* @param options Extra options to fine-tune how to serialize the path.
* @returns Path representing the state, e.g. /foo/bar?count=42.
*/
export default function getPathFromState(state: State): string {
export default function getPathFromState(
state: State,
options: Options = {}
): string {
let path = '/';

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

path += encodeURIComponent(route.name);
const config =
options[route.name] !== undefined
? (options[route.name] as { stringify?: StringifyConfig }).stringify
: undefined;

if (route.state) {
path += '/';
} else if (route.params) {
const query = [];
const params = route.params
? // Stringify all of the param values before we use them
Object.entries(route.params).reduce<{
[key: string]: string;
}>((acc, [key, value]) => {
acc[key] = config && config[key] ? config[key](value) : String(value);
return acc;
}, {})
: undefined;

for (const param in route.params) {
const value = (route.params as { [key: string]: any })[param];
if (options[route.name] !== undefined) {
const pattern =
typeof options[route.name] === 'string'
? (options[route.name] as string)
: (options[route.name] as { path: string }).path;

query.push(
`${encodeURIComponent(param)}=${encodeURIComponent(
JSON.stringify(value)
)}`
);
}
path += pattern
.split('/')
.map(p => {
const name = p.replace(/^:/, '');

path += `?${query.join('&')}`;
// If the path has a pattern for a param, put the param in the path
if (params && name in params && p.startsWith(':')) {
const value = params[name];
// Remove the used value from the params object since we'll use the rest for query string
delete params[name];
return encodeURIComponent(value);
}

return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
}

if (route.state) {
path += '/';
} else if (params) {
path += `?${queryString.stringify(params)}`;
}

current = route.state;
Expand Down
Loading

0 comments on commit 849d952

Please sign in to comment.