UNPKG

7.71 kBJavaScriptView Raw
1import * as queryString from 'query-string';
2import fromEntries from './fromEntries';
3import validatePathConfig from './validatePathConfig';
4
5const getActiveRoute = state => {
6 const route = typeof state.index === 'number' ? state.routes[state.index] : state.routes[state.routes.length - 1];
7
8 if (route.state) {
9 return getActiveRoute(route.state);
10 }
11
12 return route;
13};
14/**
15 * Utility to serialize a navigation state object to a path string.
16 *
17 * @example
18 * ```js
19 * getPathFromState(
20 * {
21 * routes: [
22 * {
23 * name: 'Chat',
24 * params: { author: 'Jane', id: 42 },
25 * },
26 * ],
27 * },
28 * {
29 * screens: {
30 * Chat: {
31 * path: 'chat/:author/:id',
32 * stringify: { author: author => author.toLowerCase() }
33 * }
34 * }
35 * }
36 * )
37 * ```
38 *
39 * @param state Navigation state to serialize.
40 * @param options Extra options to fine-tune how to serialize the path.
41 * @returns Path representing the state, e.g. /foo/bar?count=42.
42 */
43
44
45export default function getPathFromState(state, options) {
46 if (state == null) {
47 throw Error("Got 'undefined' for the navigation state. You must pass a valid state object.");
48 }
49
50 if (options) {
51 validatePathConfig(options);
52 } // Create a normalized configs object which will be easier to use
53
54
55 const configs = options !== null && options !== void 0 && options.screens ? createNormalizedConfigs(options === null || options === void 0 ? void 0 : options.screens) : {};
56 let path = '/';
57 let current = state;
58 const allParams = {};
59
60 while (current) {
61 let index = typeof current.index === 'number' ? current.index : 0;
62 let route = current.routes[index];
63 let pattern;
64 let focusedParams;
65 let focusedRoute = getActiveRoute(state);
66 let currentOptions = configs; // Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
67
68 let nestedRouteNames = [];
69 let hasNext = true;
70
71 while (route.name in currentOptions && hasNext) {
72 pattern = currentOptions[route.name].pattern;
73 nestedRouteNames.push(route.name);
74
75 if (route.params) {
76 var _currentOptions$route;
77
78 const stringify = (_currentOptions$route = currentOptions[route.name]) === null || _currentOptions$route === void 0 ? void 0 : _currentOptions$route.stringify;
79 const currentParams = fromEntries(Object.entries(route.params).map(_ref => {
80 let [key, value] = _ref;
81 return [key, stringify !== null && stringify !== void 0 && stringify[key] ? stringify[key](value) : String(value)];
82 }));
83
84 if (pattern) {
85 Object.assign(allParams, currentParams);
86 }
87
88 if (focusedRoute === route) {
89 var _pattern;
90
91 // If this is the focused route, keep the params for later use
92 // We save it here since it's been stringified already
93 focusedParams = { ...currentParams
94 };
95 (_pattern = pattern) === null || _pattern === void 0 ? void 0 : _pattern.split('/').filter(p => p.startsWith(':')) // eslint-disable-next-line no-loop-func
96 .forEach(p => {
97 const name = getParamName(p); // Remove the params present in the pattern since we'll only use the rest for query string
98
99 if (focusedParams) {
100 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
101 delete focusedParams[name];
102 }
103 });
104 }
105 } // If there is no `screens` property or no nested state, we return pattern
106
107
108 if (!currentOptions[route.name].screens || route.state === undefined) {
109 hasNext = false;
110 } else {
111 index = typeof route.state.index === 'number' ? route.state.index : route.state.routes.length - 1;
112 const nextRoute = route.state.routes[index];
113 const nestedConfig = currentOptions[route.name].screens; // if there is config for next route name, we go deeper
114
115 if (nestedConfig && nextRoute.name in nestedConfig) {
116 route = nextRoute;
117 currentOptions = nestedConfig;
118 } else {
119 // If not, there is no sense in going deeper in config
120 hasNext = false;
121 }
122 }
123 }
124
125 if (pattern === undefined) {
126 pattern = nestedRouteNames.join('/');
127 }
128
129 if (currentOptions[route.name] !== undefined) {
130 path += pattern.split('/').map(p => {
131 const name = getParamName(p); // We don't know what to show for wildcard patterns
132 // Showing the route name seems ok, though whatever we show here will be incorrect
133 // Since the page doesn't actually exist
134
135 if (p === '*') {
136 return route.name;
137 } // If the path has a pattern for a param, put the param in the path
138
139
140 if (p.startsWith(':')) {
141 const value = allParams[name];
142
143 if (value === undefined && p.endsWith('?')) {
144 // Optional params without value assigned in route.params should be ignored
145 return '';
146 }
147
148 return encodeURIComponent(value);
149 }
150
151 return encodeURIComponent(p);
152 }).join('/');
153 } else {
154 path += encodeURIComponent(route.name);
155 }
156
157 if (!focusedParams) {
158 focusedParams = focusedRoute.params;
159 }
160
161 if (route.state) {
162 path += '/';
163 } else if (focusedParams) {
164 for (let param in focusedParams) {
165 if (focusedParams[param] === 'undefined') {
166 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
167 delete focusedParams[param];
168 }
169 }
170
171 const query = queryString.stringify(focusedParams, {
172 sort: false
173 });
174
175 if (query) {
176 path += `?${query}`;
177 }
178 }
179
180 current = route.state;
181 } // Remove multiple as well as trailing slashes
182
183
184 path = path.replace(/\/+/g, '/');
185 path = path.length > 1 ? path.replace(/\/$/, '') : path;
186 return path;
187}
188
189const getParamName = pattern => pattern.replace(/^:/, '').replace(/\?$/, '');
190
191const joinPaths = function () {
192 for (var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++) {
193 paths[_key] = arguments[_key];
194 }
195
196 return [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/');
197};
198
199const createConfigItem = (config, parentPattern) => {
200 var _pattern2;
201
202 if (typeof config === 'string') {
203 // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
204 const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
205 return {
206 pattern
207 };
208 } // If an object is specified as the value (e.g. Foo: { ... }),
209 // It can have `path` property and `screens` prop which has nested configs
210
211
212 let pattern;
213
214 if (config.exact && config.path === undefined) {
215 throw new Error("A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. `path: ''`.");
216 }
217
218 pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
219 const screens = config.screens ? createNormalizedConfigs(config.screens, pattern) : undefined;
220 return {
221 // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
222 pattern: (_pattern2 = pattern) === null || _pattern2 === void 0 ? void 0 : _pattern2.split('/').filter(Boolean).join('/'),
223 stringify: config.stringify,
224 screens
225 };
226};
227
228const createNormalizedConfigs = (options, pattern) => fromEntries(Object.entries(options).map(_ref2 => {
229 let [name, c] = _ref2;
230 const result = createConfigItem(c, pattern);
231 return [name, result];
232}));
233//# sourceMappingURL=getPathFromState.js.map
\No newline at end of file