mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-06 17:34:59 +08:00
feat: add helpers to convert between url and state
This commit is contained in:
committed by
Satyajit Sahoo
parent
8ed54dace4
commit
dbe2b9159a
47
packages/core/src/__tests__/getPathFromState.test.tsx
Normal file
47
packages/core/src/__tests__/getPathFromState.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import getPathFromState from '../getPathFromState';
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane & co', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toMatchInlineSnapshot(
|
||||
`"/foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true"`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
routes: [{ name: 'bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe('/foo/bar');
|
||||
});
|
||||
92
packages/core/src/__tests__/getStateFromPath.test.tsx
Normal file
92
packages/core/src/__tests__/getStateFromPath.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
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'
|
||||
)
|
||||
).toEqual({
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'bar',
|
||||
state: {
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane & co', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles leading slash when converting', () => {
|
||||
expect(getStateFromPath('/foo/bar/?count=42')).toEqual({
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'bar',
|
||||
params: { count: 42 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles ending slash when converting', () => {
|
||||
expect(getStateFromPath('foo/bar/?count=42')).toEqual({
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'bar',
|
||||
params: { count: 42 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(getStateFromPath('foo/bar')).toEqual({
|
||||
stale: true,
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
stale: true,
|
||||
routes: [{ name: 'bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
});
|
||||
46
packages/core/src/getPathFromState.tsx
Normal file
46
packages/core/src/getPathFromState.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NavigationState, PartialState, Route } from './types';
|
||||
|
||||
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
|
||||
/**
|
||||
* Utility to serialize a navigation state object to a path string.
|
||||
*
|
||||
* @param state Navigation state to serialize.
|
||||
* @returns Path representing the state, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getPathFromState(state: State): string {
|
||||
let path = '/';
|
||||
|
||||
let current: State | undefined = state;
|
||||
|
||||
while (current) {
|
||||
const index = typeof current.index === 'number' ? current.index : 0;
|
||||
const route = current.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
};
|
||||
|
||||
path += encodeURIComponent(route.name);
|
||||
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (route.params) {
|
||||
const query = [];
|
||||
|
||||
for (const param in route.params) {
|
||||
const value = (route.params as { [key: string]: any })[param];
|
||||
|
||||
query.push(
|
||||
`${encodeURIComponent(param)}=${encodeURIComponent(
|
||||
JSON.stringify(value)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
path += `?${query.join('&')}`;
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
52
packages/core/src/getStateFromPath.tsx
Normal file
52
packages/core/src/getStateFromPath.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { InitialState } from './types';
|
||||
|
||||
/**
|
||||
* Utility to parse a path string to initial state object accepted by the container.
|
||||
* This is useful for deep linking when we need to handle the incoming URL.
|
||||
*
|
||||
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getStateFromPath(
|
||||
path: string
|
||||
): InitialState | undefined {
|
||||
const parts = path.split('?');
|
||||
const segments = parts[0].split('/').filter(Boolean);
|
||||
const query = parts[1] ? parts[1].split('&') : undefined;
|
||||
|
||||
let result: InitialState | undefined;
|
||||
let current: InitialState | undefined;
|
||||
|
||||
while (segments.length) {
|
||||
const state = {
|
||||
stale: true as true,
|
||||
routes: [{ name: decodeURIComponent(segments[0]) }],
|
||||
};
|
||||
|
||||
if (current) {
|
||||
current.routes[0].state = state;
|
||||
} else {
|
||||
result = state;
|
||||
}
|
||||
|
||||
current = state;
|
||||
segments.shift();
|
||||
}
|
||||
|
||||
if (current == null || result == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const params = query.reduce<{ [key: string]: any }>((acc, curr) => {
|
||||
const [key, value] = curr.split('=');
|
||||
|
||||
acc[decodeURIComponent(key)] = JSON.parse(decodeURIComponent(value));
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
current.routes[0].params = params;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -11,4 +11,7 @@ export { default as useNavigation } from './useNavigation';
|
||||
export { default as useFocusEffect } from './useFocusEffect';
|
||||
export { default as useIsFocused } from './useIsFocused';
|
||||
|
||||
export { default as getStateFromPath } from './getStateFromPath';
|
||||
export { default as getPathFromState } from './getPathFromState';
|
||||
|
||||
export * from './types';
|
||||
|
||||
@@ -27,12 +27,15 @@ export type NavigationState = {
|
||||
stale?: false;
|
||||
};
|
||||
|
||||
export type InitialState = Partial<Omit<NavigationState, 'routes'>> & {
|
||||
export type InitialState = Partial<
|
||||
Omit<NavigationState, 'stale' | 'routes'>
|
||||
> & {
|
||||
stale?: boolean;
|
||||
routes: Array<Omit<Route<string>, 'key'> & { state?: InitialState }>;
|
||||
};
|
||||
|
||||
export type PartialState<State extends NavigationState> = Partial<
|
||||
Omit<State, 'key' | 'routes' | 'routeNames'>
|
||||
Omit<State, 'stale' | 'key' | 'routes' | 'routeNames'>
|
||||
> & {
|
||||
stale: boolean;
|
||||
routes: Array<
|
||||
|
||||
Reference in New Issue
Block a user