Browse code
Everything is working with new model + providers, still working in redux land
Max Summe authored on 29/06/2020 03:23:24
Showing 9 changed files
Showing 9 changed files
- provider-api.js
- route-provider.js
- src/action-router.js
- src/change-url-event.js
- src/index.js
- src/provider-api.js
- src/redux-api.js
- src/route-provider.js
- src/tests/route-provider.test.js
... | ... |
@@ -196,6 +196,7 @@ function makeRoute(path, action, extraParams) { |
196 | 196 |
|
197 | 197 |
function normalizeWildcards(path) { |
198 | 198 |
let curIdx = 0; |
199 |
+ //todo curIdx doesn't increment |
|
199 | 200 |
return path.map(el => { |
200 | 201 |
if (isWildcard(el)) { |
201 | 202 |
return `:wildcard${curIdx}`; |
... | ... |
@@ -295,62 +296,77 @@ function createActionDispatcher(routesConfig, window) { |
295 | 296 |
return match ? constructAction(match) : null; |
296 | 297 |
} |
297 | 298 |
|
298 |
- const actionDispatcher = { |
|
299 |
- currentLocation: null, |
|
300 |
- store: null, |
|
299 |
+ let actionListeners = []; |
|
300 |
+ let currentPath = null; |
|
301 | 301 |
|
302 |
- activateDispatcher(store) { |
|
303 |
- window.addEventListener("urlchanged", this); |
|
304 |
- this.store = store; |
|
305 |
- }, |
|
302 |
+ function ifPathChanged(newPath, cb) { |
|
303 |
+ if (currentPath !== newPath) { |
|
304 |
+ currentPath = newPath; |
|
305 |
+ cb(); |
|
306 |
+ } |
|
307 |
+ } |
|
306 | 308 |
|
307 |
- pathForAction, |
|
309 |
+ const actionDispatcher = { |
|
308 | 310 |
|
311 |
+ pathForAction, |
|
309 | 312 |
|
310 |
- handleEvent(ev) { |
|
311 |
- if (!this.store) { |
|
312 |
- throw new Error( |
|
313 |
- "You must call activateDispatcher with redux store as argument" |
|
314 |
- ); |
|
313 |
+ //hook for everything to get action on route change |
|
314 |
+ addActionListener(cb) { |
|
315 |
+ actionListeners.push(cb); |
|
316 |
+ return () => { |
|
317 |
+ const index = R.findIndex(x => x === cb, actionListeners); |
|
318 |
+ actionListeners = R.remove(index, 1, actionListeners); |
|
315 | 319 |
} |
320 |
+ }, |
|
316 | 321 |
|
322 |
+ //needed for window event listener |
|
323 |
+ handleEvent(ev) { |
|
317 | 324 |
const location = ev.detail; |
318 | 325 |
this.receiveLocation(location); |
319 | 326 |
}, |
320 | 327 |
|
321 |
- onLocationChanged(newLoc, cb) { |
|
322 |
- if (this.currentLocation !== newLoc) { |
|
323 |
- this.currentLocation = newLoc; |
|
324 |
- cb(); |
|
325 |
- } |
|
326 |
- }, |
|
327 |
- |
|
328 | 328 |
receiveLocation(location) { |
329 |
- this.onLocationChanged(location.pathname, () => { |
|
329 |
+ ifPathChanged(location.pathname, () => { |
|
330 | 330 |
|
331 | 331 |
const action = actionForLocation(location); |
332 | 332 |
|
333 | 333 |
if (action) { |
334 |
- this.store.dispatch(action); |
|
334 |
+ actionListeners.forEach(cb => cb(action)); |
|
335 | 335 |
} |
336 | 336 |
}); |
337 | 337 |
}, |
338 |
+ // necessary for new APIs that aren't redux-focused - need to propagate |
|
339 |
+ navigateToRoute(route, params) { |
|
340 |
+ const action = {type: route, ...params}; |
|
341 |
+ const newPath = matchesAction(action, compiledActionMatchers) |
|
342 |
+ ? pathForAction(action) |
|
343 |
+ : null; |
|
344 |
+ |
|
345 |
+ if (newPath) { |
|
346 |
+ ifPathChanged(newPath, () => { |
|
347 |
+ window.history.pushState({}, "", newPath); |
|
348 |
+ actionListeners.forEach(cb => cb(action)); |
|
349 |
+ }); |
|
350 |
+ } |
|
351 |
+ }, |
|
338 | 352 |
|
353 |
+ // can this be simplified to get rid of fundamental action model? |
|
339 | 354 |
receiveAction(action) { |
340 |
- const path = pathForAction(action); |
|
355 |
+ const newPath = matchesAction(action, compiledActionMatchers) |
|
356 |
+ ? pathForAction(action) |
|
357 |
+ : null; |
|
341 | 358 |
|
342 |
- if (path) { |
|
343 |
- this.onLocationChanged(path, () => { |
|
344 |
- window.history.pushState({}, "", path); |
|
359 |
+ if (newPath) { |
|
360 |
+ ifPathChanged(newPath, () => { |
|
361 |
+ window.history.pushState({}, "", newPath); |
|
345 | 362 |
}); |
346 | 363 |
} |
347 | 364 |
}, |
348 | 365 |
|
349 |
- handlesAction(action) { |
|
350 |
- return matchesAction(action, compiledActionMatchers); |
|
351 |
- } |
|
352 | 366 |
}; |
353 | 367 |
|
368 |
+ window.addEventListener("urlchanged", actionDispatcher); |
|
369 |
+ |
|
354 | 370 |
return actionDispatcher; |
355 | 371 |
} |
356 | 372 |
|
... | ... |
@@ -1,14 +1,16 @@ |
1 | 1 |
import addMissingHistoryEvents from "./history-events"; |
2 | 2 |
import addChangeUrlEvent from "./change-url-event"; |
3 |
-import installRouter from "./action-router"; |
|
3 |
+import installBrowserRouter from "./redux-api"; |
|
4 | 4 |
import Fragment from "./fragment"; |
5 | 5 |
import ActionLink from "./action-link"; |
6 |
+import {createActionDispatcher} from "./action-router"; |
|
6 | 7 |
|
7 | 8 |
addMissingHistoryEvents(window, window.history); |
8 | 9 |
addChangeUrlEvent(window); |
9 | 10 |
|
10 |
-const installBrowserRouter = function(routesConfig) { |
|
11 |
- return installRouter(routesConfig, window); |
|
11 |
+export { |
|
12 |
+ installBrowserRouter, |
|
13 |
+ Fragment, |
|
14 |
+ ActionLink, |
|
15 |
+ createActionDispatcher |
|
12 | 16 |
}; |
13 |
- |
|
14 |
-export { installBrowserRouter, Fragment, ActionLink}; |
15 | 17 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,61 @@ |
1 |
+import * as R from 'ramda'; |
|
2 |
+import React, {useReducer, useMemo, useEffect} from 'react'; |
|
3 |
+ |
|
4 |
+const RouteContext = React.createContext(null); |
|
5 |
+const ActionDispatcherContext = React.createContext(null); |
|
6 |
+ |
|
7 |
+function RouteProvider({children, actionDispatcher, _window}) { |
|
8 |
+ |
|
9 |
+ const [route, updateRoute] = useReducer((state, action) => |
|
10 |
+ R.omit(['type'], R.assoc('routeName', action.type, action)) |
|
11 |
+ , {}); |
|
12 |
+ |
|
13 |
+ useEffect(() => { |
|
14 |
+ return actionDispatcher.addActionListener(action => updateRoute(action)); |
|
15 |
+ }); |
|
16 |
+ |
|
17 |
+ useEffect(() => { |
|
18 |
+ actionDispatcher.receiveLocation(_window.location); |
|
19 |
+ }); |
|
20 |
+ |
|
21 |
+ return (<ActionDispatcherContext.Provider value={actionDispatcher}> |
|
22 |
+ <RouteContext.Provider value={route}> |
|
23 |
+ {children} |
|
24 |
+ </RouteContext.Provider> |
|
25 |
+ </ActionDispatcherContext.Provider>); |
|
26 |
+} |
|
27 |
+ |
|
28 |
+RouteProvider.defaultProps = { |
|
29 |
+ _window: window ? window : null |
|
30 |
+}; |
|
31 |
+ |
|
32 |
+function withRoute(Component) { |
|
33 |
+ return function ({children, ...restProps}) { |
|
34 |
+ return (<RouteContext.Consumer> |
|
35 |
+ {route => |
|
36 |
+ <Component {...{...restProps, route}}>{children}</Component> |
|
37 |
+ } |
|
38 |
+ </RouteContext.Consumer> |
|
39 |
+ ) |
|
40 |
+ } |
|
41 |
+} |
|
42 |
+ |
|
43 |
+function RouteLink({route, params, children, ...props}) { |
|
44 |
+ const action = { |
|
45 |
+ type: route, ...params |
|
46 |
+ }; |
|
47 |
+ return <ActionDispatcherContext.Consumer>{ |
|
48 |
+ dispatcher => { |
|
49 |
+ const url = dispatcher.pathForAction(action); |
|
50 |
+ return <a |
|
51 |
+ onClick={(ev) => { |
|
52 |
+ ev.preventDefault(); |
|
53 |
+ dispatcher.navigateToRoute(route, params); |
|
54 |
+ }} |
|
55 |
+ href={url} {...props}>{children}</a> |
|
56 |
+ } |
|
57 |
+ } |
|
58 |
+ </ActionDispatcherContext.Consumer>; |
|
59 |
+} |
|
60 |
+ |
|
61 |
+export {RouteProvider, withRoute, RouteLink}; |
... | ... |
@@ -2,9 +2,8 @@ import {createActionDispatcher} from "./action-router"; |
2 | 2 |
|
3 | 3 |
function buildMiddleware(actionDispatcher) { |
4 | 4 |
return store => next => action => { |
5 |
- if (actionDispatcher.handlesAction(action)) { |
|
6 |
- actionDispatcher.receiveAction(action, store); |
|
7 |
- } |
|
5 |
+ actionDispatcher.receiveAction(action, store); |
|
6 |
+ |
|
8 | 7 |
return next(action); |
9 | 8 |
}; |
10 | 9 |
} |
... | ... |
@@ -16,7 +15,7 @@ function enhanceStoreCreator(actionDispatcher) { |
16 | 15 |
return (reducer, finalInitialState, enhancer) => { |
17 | 16 |
const theStore = nextStoreCreator(reducer, finalInitialState, enhancer); |
18 | 17 |
|
19 |
- actionDispatcher.activateDispatcher(theStore); |
|
18 |
+ actionDispatcher.addActionListener((action) => theStore.dispatch(action)); |
|
20 | 19 |
|
21 | 20 |
theStore.dispatch = middleware(theStore)( |
22 | 21 |
theStore.dispatch.bind(theStore) |
... | ... |
@@ -26,8 +25,9 @@ function enhanceStoreCreator(actionDispatcher) { |
26 | 25 |
}; |
27 | 26 |
} |
28 | 27 |
|
29 |
-export default function installBrowserRouter(routesConfig, window) { |
|
30 |
- const actionDispatcher = createActionDispatcher(routesConfig, window); |
|
28 |
+ |
|
29 |
+export default function installBrowserRouter(routesConfig, _window = window) { |
|
30 |
+ const actionDispatcher = createActionDispatcher(routesConfig, _window); |
|
31 | 31 |
|
32 | 32 |
const middleware = x => { |
33 | 33 |
//eslint-disable-next-line no-console |
... | ... |
@@ -44,7 +44,7 @@ export default function installBrowserRouter(routesConfig, window) { |
44 | 44 |
enhancer: enhanceStoreCreator(actionDispatcher), |
45 | 45 |
init: actionDispatcher.receiveLocation.bind( |
46 | 46 |
actionDispatcher, |
47 |
- window.location |
|
47 |
+ _window.location |
|
48 | 48 |
), |
49 | 49 |
_actionDispatcher: actionDispatcher |
50 | 50 |
}; |
51 | 51 |
deleted file mode 100644 |
... | ... |
@@ -1,62 +0,0 @@ |
1 |
-import React, {useReducer, useMemo, useEffect} from 'react'; |
|
2 |
-import {createActionDispatcher} from "./action-router"; |
|
3 |
- |
|
4 |
-export function createRouteProvider(routesConfig, _window = window) { |
|
5 |
- |
|
6 |
- const RoutingContext = React.createContext(null); |
|
7 |
- const actionDispatcher = createActionDispatcher(routesConfig, _window) |
|
8 |
- |
|
9 |
- |
|
10 |
- function RouteProvider({children}) { |
|
11 |
- |
|
12 |
- const [route, updateRoute] = useReducer((state, action) => action, {}); |
|
13 |
- const store = useMemo(() => { |
|
14 |
- return {dispatch: (action) => { |
|
15 |
- updateRoute(action); |
|
16 |
- }} |
|
17 |
- }, [updateRoute]); |
|
18 |
- |
|
19 |
- useEffect(() => { |
|
20 |
- actionDispatcher.activateDispatcher(store); |
|
21 |
- }, [store]); |
|
22 |
- |
|
23 |
- useEffect(() => { |
|
24 |
- actionDispatcher.receiveLocation(_window.location); |
|
25 |
- }); |
|
26 |
- |
|
27 |
- return <RoutingContext.Provider value={route}> |
|
28 |
- {children} |
|
29 |
- </RoutingContext.Provider> |
|
30 |
- |
|
31 |
- } |
|
32 |
- |
|
33 |
- function withRoute(Component) { |
|
34 |
- return function ({children, ...restProps}) { |
|
35 |
- return (<RoutingContext.Consumer> |
|
36 |
- {route => |
|
37 |
- <Component {...{...restProps, route}}>{children}</Component> |
|
38 |
- } |
|
39 |
- </RoutingContext.Consumer> |
|
40 |
- ) |
|
41 |
- } |
|
42 |
- } |
|
43 |
- |
|
44 |
- function routeToUrl(routeName, params) { |
|
45 |
- return actionDispatcher.pathForAction({ |
|
46 |
- type: routeName, ...params |
|
47 |
- }) |
|
48 |
- } |
|
49 |
- |
|
50 |
- function RouteLink({route, params, children, ...props}) { |
|
51 |
- const url = routeToUrl(route, params); |
|
52 |
- |
|
53 |
- return <a href={url} {...props}>{children}</a>; |
|
54 |
- } |
|
55 |
- |
|
56 |
- const init = actionDispatcher.receiveLocation.bind( |
|
57 |
- actionDispatcher, |
|
58 |
- _window.location |
|
59 |
- ); |
|
60 |
- |
|
61 |
- return {RouteProvider, withRoute, routeToUrl, RouteLink}; |
|
62 |
-} |