git.fiddlerwoaroof.com
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
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+module.exports = require('./dist/provider-api');
2
+
0 3
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-console.log('woo');
2
-module.exports = require('./dist/route-provider');
3
-
4
-console.log('blarge', module.exports);
... ...
@@ -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,6 +1,6 @@
1 1
 import * as R from "ramda";
2 2
 
3
-export function wrapEvent(target, name, obj) {
3
+function wrapEvent(target, name, obj) {
4 4
   target.addEventListener(name, obj);
5 5
 }
6 6
 
... ...
@@ -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
-}
63 0
similarity index 100%
64 1
rename from src/tests/route-provider.test.js
65 2
rename to src/tests/provider-api.test.js