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:
Wojciech Lewicki
2020-01-29 16:21:35 +01:00
committed by osdnk
parent ea66b1a3b8
commit b931ae62df
5 changed files with 193 additions and 108 deletions

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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;
};
}
}

View File

@@ -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;
}