Bläddra i källkod

NamedRoute and NamedLink

asmer 3 månader sedan
förälder
incheckning
1656af2bba
7 ändrade filer med 303 tillägg och 28 borttagningar
  1. 1 1
      README.md
  2. 146 1
      package-lock.json
  3. 2 1
      package.json
  4. 47 25
      src/App.tsx
  5. 1 0
      src/lib/index.ts
  6. 60 0
      src/lib/router/index.tsx
  7. 46 0
      src/test/pub.tsx

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
 # Rayers. React layers.
 
-[Description](http://doc.a-level.com.ua/react-layers/8)
+[Description](http://doc.a-level.com.ua/react-layers)
 

+ 146 - 1
package-lock.json

@@ -9,7 +9,8 @@
       "version": "0.0.0",
       "dependencies": {
         "react": "^18.3.1",
-        "react-dom": "^18.3.1"
+        "react-dom": "^18.3.1",
+        "react-router-dom": "^5.3.4"
       },
       "devDependencies": {
         "@types/react": "^18.3.3",
@@ -311,6 +312,18 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
+      "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
+      "license": "MIT",
+      "dependencies": {
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.25.0",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
@@ -2454,6 +2467,29 @@
         "node": ">=4"
       }
     },
+    "node_modules/history": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.1.2",
+        "loose-envify": "^1.2.0",
+        "resolve-pathname": "^3.0.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0",
+        "value-equal": "^1.0.1"
+      }
+    },
+    "node_modules/hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
     "node_modules/ignore": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2553,6 +2589,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+      "license": "MIT"
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2775,6 +2817,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2878,6 +2929,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/path-to-regexp": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+      "license": "MIT",
+      "dependencies": {
+        "isarray": "0.0.1"
+      }
+    },
     "node_modules/path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -2947,6 +3007,17 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3003,6 +3074,12 @@
         "react": "^18.3.1"
       }
     },
+    "node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "license": "MIT"
+    },
     "node_modules/react-refresh": {
       "version": "0.14.2",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -3013,6 +3090,50 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/react-router": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
+      "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "hoist-non-react-statics": "^3.1.0",
+        "loose-envify": "^1.3.1",
+        "path-to-regexp": "^1.7.0",
+        "prop-types": "^15.6.2",
+        "react-is": "^16.6.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=15"
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
+      "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.2",
+        "react-router": "5.3.4",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=15"
+      }
+    },
+    "node_modules/regenerator-runtime": {
+      "version": "0.14.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+      "license": "MIT"
+    },
     "node_modules/resolve-from": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3023,6 +3144,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/resolve-pathname": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
+      "license": "MIT"
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -3222,6 +3349,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tiny-invariant": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+      "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+      "license": "MIT"
+    },
+    "node_modules/tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
+      "license": "MIT"
+    },
     "node_modules/to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -3339,6 +3478,12 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/value-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
+      "license": "MIT"
+    },
     "node_modules/vite": {
       "version": "5.3.5",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",

+ 2 - 1
package.json

@@ -11,7 +11,8 @@
   },
   "dependencies": {
     "react": "^18.3.1",
-    "react-dom": "^18.3.1"
+    "react-dom": "^18.3.1",
+    "react-router-dom": "^5.3.4"
   },
   "devDependencies": {
     "@types/react": "^18.3.3",

+ 47 - 25
src/App.tsx

@@ -2,44 +2,66 @@ import { useState, useEffect } from 'react'
 import reactLogo from './assets/react.svg'
 import viteLogo from '/vite.svg'
 import './App.css'
+import {createBrowserHistory} from 'history';
+import {Router} from 'react-router-dom'
+import {NamedRoute, NamedLink} from './lib';
 
-import {createPub, usePub} from './lib';
-import type { Pub } from './lib';
+const history = createBrowserHistory()
 
+const SwapiPeople = ({data:{name, eye_color}}) =>
+<div>
+    <h3 style={{color: eye_color}}>{name}</h3>
+</div>
 
-const testPub:Pub = createPub({count: 0})
+const SwapiPlanet = ({data:{name, diameter}}) =>
+<div>
+    <h3>{name}</h3>
+    <h4>{diameter}</h4>
+</div>
 
-testPub.subscribe(() => console.log(testPub.count))
+const queries = {
+    people({id}){
+        return fetch('https://swapi.dev/api/people/' + id).then(res => res.json())
+    },
+    planet({id}){
+        if (id == 2) 
+            return {
+                "name": "Alderaan",
+                "diameter": "12500"
+            }
+        return fetch('https://swapi.dev/api/planets/' + id).then(res => res.json())
+    }
+}
 
-setInterval(() => testPub.count--, 5000)
+const Pending = () => <h1>Loading</h1>
+const Error   = ({error}) => <h1>Error: {error.message}</h1>
 
 
 function App() {
-    const {count} = usePub(testPub)
-
   return (
-    <>
+    <Router history={history}>
       <div>
-        <a href="https://vitejs.dev" target="_blank">
-          <img src={viteLogo} className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://react.dev" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
-      <h1>Vite + React</h1>
-      <div className="card">
-        <button onClick={() => testPub.count++}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
+        <NamedRoute name="people" 
+                    query={queries.people}
+                    path="/people/:id" 
+                    pendingQueryComponent={Pending}
+                    component={SwapiPeople}/>
+        <NamedRoute routeName="planet" 
+                    query={queries.planet}
+                    pendingQueryComponent={Pending}
+                    errorQueryComponent={Error}
+                    path="/planet/:id" 
+                    component={SwapiPlanet}/>
       </div>
       <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
+        <NamedLink routeName="people" params={{id: 1}}>Luke</NamedLink>
+        <NamedLink routeName="people" params={{id: 2}}>C3PO</NamedLink>
+      </p>
+      <p className="read-the-docs">
+        <NamedLink routeName="planet" params={{id: 1}}>Tatooin</NamedLink>
+        <NamedLink routeName="planet" params={{id: 2}}>Alderaan</NamedLink>
       </p>
-    </>
+    </Router>
   )
 }
 

+ 1 - 0
src/lib/index.ts

@@ -4,3 +4,4 @@ export {createPub};
 
 export type { Pub } from  './pub'
 export { usePub } from  './pub'
+export {NamedRoute, NamedLink} from './router'

+ 60 - 0
src/lib/router/index.tsx

@@ -0,0 +1,60 @@
+import {ComponentType} from 'react';
+import {useState, useEffect} from 'react';
+import {Route, Link, generatePath} from 'react-router-dom';
+
+const routes = {
+
+}
+
+export const NamedRoute = ({name, routeName, query, pendingQueryComponent:P, errorQueryComponent:E, errorQueryComponentErrorProp="error", componentQueryResultProp='data', path, component, render, ...props}) => {
+    name  ||= routeName
+    routes[name || path] = {...props, path}
+
+    const Render:ComponentType = component || render 
+
+    const ComponentWrapper = (props) => {
+        const [queryResult, setQueryResult] = useState()
+        const [queryError, setQueryError] = useState()
+        useEffect(() => {
+            if (query && typeof query === 'function'){
+                const querySyncResult = query(props.match.params, props, name)
+                if (querySyncResult && typeof querySyncResult.then === 'function'){
+                    querySyncResult.then(setQueryResult, setQueryError)
+                }
+                else {
+                    setQueryResult(querySyncResult)
+                }
+            }
+            return () => {
+                setQueryError()
+                setQueryResult()
+            }
+        },[props])
+
+        if (!query || typeof query !== 'function')
+            return <Render {...{[componentQueryResultProp]:query, ...props}} />
+
+        if (!queryResult && !queryError)
+            return P ? <P {...props} /> : null
+
+        if (queryError)
+            return E ? <E {...{[errorQueryComponentErrorProp]: queryError, ...props}} /> : null
+
+        console.log({[componentQueryResultProp]:queryResult, ...props})
+
+        return <Render {...{[componentQueryResultProp]:queryResult, ...props}} />
+    }
+
+    return <Route {...props} path={path}  component={ComponentWrapper}/>
+}
+
+export const NamedLink = ({routeName, params, to,...props}) => {
+    const route = routes[routeName]
+    if (route && !to){
+        const pattern = route.path
+        to            = generatePath(pattern, params)
+    }
+    return <Link to={to} {...props} />
+}
+
+

+ 46 - 0
src/test/pub.tsx

@@ -0,0 +1,46 @@
+import { useState, useEffect } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from '/vite.svg'
+import './App.css'
+
+import {createPub, usePub} from './lib';
+import type { Pub } from './lib';
+
+
+const testPub:Pub = createPub({count: 0})
+
+testPub.subscribe(() => console.log(testPub.count))
+
+setInterval(() => testPub.count--, 5000)
+
+
+function App() {
+    const {count} = usePub(testPub)
+
+  return (
+    <>
+      <div>
+        <a href="https://vitejs.dev" target="_blank">
+          <img src={viteLogo} className="logo" alt="Vite logo" />
+        </a>
+        <a href="https://react.dev" target="_blank">
+          <img src={reactLogo} className="logo react" alt="React logo" />
+        </a>
+      </div>
+      <h1>Vite + React</h1>
+      <div className="card">
+        <button onClick={() => testPub.count++}>
+          count is {count}
+        </button>
+        <p>
+          Edit <code>src/App.tsx</code> and save to test HMR
+        </p>
+      </div>
+      <p className="read-the-docs">
+        Click on the Vite and React logos to learn more
+      </p>
+    </>
+  )
+}
+
+export default App