mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 20:35:19 +08:00
feat: add screens prop for nested configs (#308)
Nested configs' names with their configs are now in `screens` property of the route object.
This commit is contained in:
@@ -116,10 +116,13 @@ it("doesn't add query param for empty params", () => {
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens', () => {
|
||||
const path = '/few/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
@@ -178,10 +181,13 @@ it('handles state with config with nested screens', () => {
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and unused configs', () => {
|
||||
const path = '/few/baz/jane?answer=42&count=10&valid=true';
|
||||
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
@@ -230,19 +236,31 @@ it('handles state with config with nested screens and unused configs', () => {
|
||||
});
|
||||
|
||||
it('handles nested object with stringify in it', () => {
|
||||
const path = '/bar/sweet/apple/few/bis/jane?answer=42&count=10&valid=true';
|
||||
const path = '/bar/sweet/apple/foe/bis/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -292,7 +310,7 @@ it('handles nested object with stringify in it', () => {
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth', () => {
|
||||
@@ -300,9 +318,14 @@ it('handles nested object for second route depth', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -326,7 +349,7 @@ it('handles nested object for second route depth', () => {
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and and path and stringify in roots', () => {
|
||||
@@ -337,16 +360,20 @@ it('handles nested object for second route depth and and path and stringify in r
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -370,5 +397,5 @@ it('handles nested object for second route depth and and path and stringify in r
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
@@ -35,9 +35,9 @@ it('converts path string to initial state', () => {
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config', () => {
|
||||
const path = '/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const path = '/foo/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: 'few',
|
||||
Foo: 'foo',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
@@ -141,10 +141,13 @@ it('handles route without param', () => {
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens', () => {
|
||||
const path = '/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
@@ -203,10 +206,13 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens and unused parse functions', () => {
|
||||
const path = '/few/baz/jane?count=10&answer=42&valid=true';
|
||||
const path = '/foe/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
@@ -254,21 +260,31 @@ it('converts path string to initial state with config with nested screens and un
|
||||
});
|
||||
|
||||
it('handles nested object with unused configs and with parse in it', () => {
|
||||
const path = '/bar/sweet/apple/few/bis/jane?count=10&answer=42&valid=true';
|
||||
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -328,9 +344,14 @@ it('handles parse in nested object for second route depth', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -370,16 +391,20 @@ it('handles parse in nested object for second route depth and and path and parse
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -407,3 +432,34 @@ it('handles parse in nested object for second route depth and and path and parse
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if path is empty', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
starting: true,
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,11 @@ type StringifyConfig = Record<string, (value: any) => string>;
|
||||
type Options = {
|
||||
[routeName: string]:
|
||||
| string
|
||||
| { path: string; stringify?: StringifyConfig }
|
||||
| Options;
|
||||
| {
|
||||
path: string;
|
||||
stringify?: StringifyConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,6 +46,9 @@ export default function getPathFromState(
|
||||
state?: State,
|
||||
options: Options = {}
|
||||
): string {
|
||||
if (state === undefined) {
|
||||
throw Error('NavigationState not passed');
|
||||
}
|
||||
let path = '/';
|
||||
|
||||
let current: State | undefined = state;
|
||||
@@ -50,7 +56,7 @@ export default function getPathFromState(
|
||||
while (current) {
|
||||
let index = typeof current.index === 'number' ? current.index : 0;
|
||||
let route = current.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
state?: State;
|
||||
};
|
||||
let currentOptions = options;
|
||||
let pattern = route.name;
|
||||
@@ -64,10 +70,14 @@ export default function getPathFromState(
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
break;
|
||||
} else {
|
||||
currentOptions = currentOptions[route.name] as Options;
|
||||
if (!(currentOptions[route.name] as { screens?: Options }).screens) {
|
||||
throw Error('Wrong Options object passed');
|
||||
}
|
||||
currentOptions = (currentOptions[route.name] as { screens: Options })
|
||||
.screens;
|
||||
index = typeof route.state.index === 'number' ? route.state.index : 0;
|
||||
route = route.state.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
state?: State;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ import { NavigationState, PartialState, InitialState } from './types';
|
||||
type ParseConfig = Record<string, (value: string) => any>;
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; parse?: ParseConfig } | Options;
|
||||
[routeName: string]:
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
parse?: ParseConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
};
|
||||
|
||||
type RouteConfig = {
|
||||
@@ -42,6 +48,9 @@ export default function getStateFromPath(
|
||||
path: string,
|
||||
options: Options = {}
|
||||
): ResultState | undefined {
|
||||
if (path === '') {
|
||||
return undefined;
|
||||
}
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = ([] as RouteConfig[]).concat(
|
||||
...Object.keys(options).map(key => createNormalizedConfigs(key, options))
|
||||
@@ -65,7 +74,7 @@ export default function getStateFromPath(
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeNames = config.routeNames;
|
||||
routeNames = [...config.routeNames];
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
.split('/')
|
||||
@@ -164,7 +173,7 @@ export default function getStateFromPath(
|
||||
const route = current.routes[0];
|
||||
|
||||
const params = queryString.parse(query);
|
||||
const parseFunction = findParseConfigForRoute(route.name, options);
|
||||
const parseFunction = findParseConfigForRoute(route.name, configs);
|
||||
|
||||
if (parseFunction) {
|
||||
Object.keys(params).forEach(name => {
|
||||
@@ -185,7 +194,7 @@ function createNormalizedConfigs(
|
||||
routeConfig: Options,
|
||||
routeNames: string[] = []
|
||||
): RouteConfig[] {
|
||||
const configs = [];
|
||||
const configs: RouteConfig[] = [];
|
||||
|
||||
routeNames.push(key);
|
||||
|
||||
@@ -196,30 +205,19 @@ function createNormalizedConfigs(
|
||||
configs.push(createConfigItem(routeNames, value));
|
||||
} else if (typeof value === 'object') {
|
||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||
// it could have config object and optionally nested config
|
||||
Object.keys(value).forEach(nestedKey => {
|
||||
if (nestedKey === 'path') {
|
||||
configs.push(
|
||||
createConfigItem(
|
||||
routeNames,
|
||||
value[nestedKey] as string,
|
||||
value.parse ? (value.parse as ParseConfig) : undefined
|
||||
)
|
||||
);
|
||||
} else if (nestedKey === 'parse') {
|
||||
// We handle custom parse function when a `path` is specified (in nestedKey === path)
|
||||
} else {
|
||||
// If the name of the key is not `path` or `parse`, it's a nested config for route
|
||||
// So we need to traverse into it and collect the configs
|
||||
// it has `path` property and
|
||||
// it could have `screens` prop which has nested configs
|
||||
configs.push(createConfigItem(routeNames, value.path, value.parse));
|
||||
if (value.screens) {
|
||||
Object.keys(value.screens).forEach(nestedConfig => {
|
||||
const result = createNormalizedConfigs(
|
||||
nestedKey,
|
||||
routeConfig[key] as Options,
|
||||
nestedConfig,
|
||||
value.screens as Options,
|
||||
routeNames
|
||||
);
|
||||
|
||||
configs.push(...result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
routeNames.pop();
|
||||
@@ -247,21 +245,12 @@ function createConfigItem(
|
||||
|
||||
function findParseConfigForRoute(
|
||||
routeName: string,
|
||||
config: Options
|
||||
flatConfig: RouteConfig[]
|
||||
): ParseConfig | undefined {
|
||||
if (config[routeName]) {
|
||||
return (config[routeName] as { parse?: ParseConfig }).parse;
|
||||
}
|
||||
|
||||
for (const name in config) {
|
||||
if (typeof config[name] === 'object') {
|
||||
const parse = findParseConfigForRoute(routeName, config[name] as Options);
|
||||
|
||||
if (parse) {
|
||||
return parse;
|
||||
}
|
||||
for (const config of flatConfig) {
|
||||
if (routeName === config.routeNames[config.routeNames.length - 1]) {
|
||||
return config.parse;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user