crossroad 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.min.js CHANGED
@@ -1 +1 @@
1
- import t,{createContext as e,useState as r,useEffect as n,useContext as a}from"react";var i=e();const o=t=>{if("string"!=typeof t)return t;const e={},r=new URL(t,"http://localhost:3000/");e.path=r.pathname.replace(/\/$/,"")||"/",e.query={};for(const[t]of r.searchParams)e.query[t]=(n=r.searchParams.getAll(t)).length>1?n:n[0];var n;return r.hash&&(e.hash=r.hash.replace(/^#/,"")),e},s=t=>{if("string"==typeof t)return t;const{path:e,query:r={},hash:n}=t||{};let a=e||"/";const i=new URLSearchParams(Object.entries(r).filter(([t,e])=>e).map(([t,e])=>(Array.isArray(e)?e:[e]).map(e=>[t,e])).flat()).toString();return i&&(a+="?"+i),n&&(a+="#"+n),a};var l=()=>"undefined"==typeof window;function p(t,e,r={}){if(t=JSON.parse(JSON.stringify(o(t))),(e=JSON.parse(JSON.stringify(o(e)))).path=e.path.replace(/\/$/,"")||"/",t.path=t.path.replace(/\/$/,"")||"/",t.path.endsWith("*")){t.path=t.path.replace(/\/?\*/,"")||"/";const r=t.path.split("/").filter(Boolean).length;e.path="/"+e.path.slice(1).split("/").slice(0,r).join("/")}if(Object.entries(t.query).length)for(let r in t.query){if(!(r in e.query))return!1;if(t.query[r]&&t.query[r]!==e.query[r])return!1}if(!t.path.includes(":"))return t.path===e.path&&r;if(t.path.split("/").length!==e.path.split("/").length)return!1;const n={},a=t.path.split("/").every((t,a)=>{const i=e.path.split("/")[a];return t.startsWith(":")?(n[t.slice(1)]=i,r):i===t});return a&&Object.assign(r,n),a&&r}var h=({path:e="*",exact:r=!0,component:n,render:o,children:s})=>{const l=a(i),h=p(e,l[0]);if(!h)return null;if(n){const e=n;s=t.createElement(e,h)}else if(o)s=o(h);else if(!s)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(i.Provider,{value:[{...l[0],params:h},...l.slice(1)]},s)},c=()=>{const t=a(i);if(!t)throw new Error("Wrap your App with <Router>");return t};var u=({redirect:t,children:e})=>{const[r,a]=c(),i=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>p(t.props.path||"*",r))||null;return n(()=>{t&&(i||("function"==typeof t&&(t=t(r)),a(s(t))))},[t,i]),i},f=()=>{const[t,e]=c();return[t.path,(r,n)=>e({...t,path:r},n)]},d=t=>{const[e,r]=c(),n=e.query,a=(t,n)=>r({...e,query:t},n);return t?[n[t],(e,r)=>{if(null===e){const{[t]:e,...i}=n;return a(i,r)}return a({...n,[t]:e},r)}]:[n,a]},y=()=>{const[t,e]=c();return[t.hash,(r,n)=>e({...t,hash:r},n)]},m=t=>{const e=a(i);return t?p(t,e[0].path)||{}:e[0].params};export default({url:e,children:a})=>{const p=e||(l()?"/":window.location.href),[h,c]=r(()=>o(p)),u=(t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);history[e+"State"]({},null,s(t)),c(o(t))};return n(()=>{if(l())return;const t=()=>c(o(window.location.href)),e=t=>{const e=(t=>{if(!t)return null;const e=t.getAttribute("href");return e?/^https?:\/\//.test(e)||null!==t.getAttribute("target")?null:e:null})(t.target.closest("a"));e&&(t.preventDefault(),u(e))};return window.addEventListener("popstate",t),document.addEventListener("click",e),()=>{window.removeEventListener("popstate",t),document.removeEventListener("click",e)}},[]),t.createElement(i.Provider,{value:[h,u]},a)};export{i as Context,h as Route,u as Switch,y as useHash,m as useParams,f as usePath,d as useQuery,c as useUrl};
1
+ import t,{createContext as e,useState as r,useCallback as n,useEffect as a,useContext as o}from"react";var i=e();const s=t=>{if("string"!=typeof t)return t;const e={},r=new URL(t,"http://localhost:3000/");e.path=r.pathname.replace(/\/$/,"")||"/",e.query={};for(const[t]of r.searchParams)e.query[t]=(n=r.searchParams.getAll(t)).length>1?n:n[0];var n;return r.hash&&(e.hash=r.hash.replace(/^#/,"")),e},p=t=>{if("string"==typeof t)return t;const{path:e,query:r={},hash:n}=t||{};let a=e||"/";const o=new URLSearchParams(Object.entries(r).map(([t,e])=>(Array.isArray(e)?e:[e]).map(e=>[t,e])).flat().filter(([t,e])=>e)).toString();return o&&(a+="?"+o),n&&(a+="#"+n),a};var u=()=>"undefined"==typeof window;function c(t,e,r={}){if(t=JSON.parse(JSON.stringify(s(t))),(e=JSON.parse(JSON.stringify(s(e)))).path=e.path.replace(/\/$/,"")||"/",t.path=t.path.replace(/\/$/,"")||"/",t.path.endsWith("*")){t.path=t.path.replace(/\/?\*/,"")||"/";const r=t.path.split("/").filter(Boolean).length;e.path="/"+e.path.slice(1).split("/").slice(0,r).join("/")}if(Object.entries(t.query).length)for(let r in t.query){if(!(r in e.query))return!1;if(t.query[r]&&t.query[r]!==e.query[r])return!1}if(!t.path.includes(":"))return t.path===e.path&&r;if(t.path.split("/").length!==e.path.split("/").length)return!1;const n={},a=t.path.split("/").every((t,a)=>{const o=e.path.split("/")[a];return t.startsWith(":")?(n[t.slice(1)]=o,r):o===t});return a&&Object.assign(r,n),a&&r}var l=({path:e="*",exact:r=!0,component:n,render:a,children:s})=>{const p=o(i),u=c(e,p[0]);if(!u)return null;if(n){const e=n;s=t.createElement(e,u)}else if(a)s=a(u);else if(!s)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(i.Provider,{value:[{...p[0],params:u},...p.slice(1)]},s)},h=()=>{const t=o(i);if(!t)throw new Error("Wrap your App with <Router>");return t};var f=({redirect:t,children:e})=>{const[r,n]=h(),o=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>c(t.props.path||"*",r))||null;return a(()=>{t&&(o||("function"==typeof t&&(t=t(r)),n(p(t))))},[t,o]),o},y=()=>{const[t,e]=h(),r=n((t,r)=>{e(e=>("function"==typeof t&&(t=t(e.path)),"string"!=typeof t&&(t="/"),{...e,path:t}),r)},[]);return[t.path,r]};const d=t=>p({query:t});var q=t=>t?(t=>{const[e,r]=h(),a=n((e,n)=>{r(r=>{const n=r.query[t];if((e="function"==typeof e?e(n):e)===n)return r;if(e)return{...r,query:{...r.query,[t]:e}};{const{[t]:e,...n}=r.query;return{...r,query:n}}},n)},[]);return[e.query[t],a]})(t):(()=>{const[t,e]=h(),r=n((t,r)=>{e(e=>("string"==typeof(t="function"==typeof t?t(e.query):t)&&(t=s("/?"+t.replace(/^\?/,"")).query),t=s(d(t)).query,d(t)===d(e.query)?e:{...e,query:t}),r)},[]);return[t.query,r]})(),m=()=>{const[t,e]=h(),r=n((t,r)=>{e(e=>("function"==typeof t&&(t=t(e.hash)),"string"!=typeof t&&(t=""),t=t.replace(/^#/,""),{...e,hash:t}),r)},[]);return[t.hash,r]},g=t=>{const e=o(i);return t?c(t,e[0].path)||{}:e[0].params};export default({url:e,children:o})=>{const c=e||(u()?"/":window.location.href),[l,h]=r(()=>s(c)),f=n((t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);t="function"==typeof t?t(l):t,h(r=>p(r)===p(t)?r:(history[e+"State"]({},null,p(t)),s(t)))},[]);return a(()=>{if(u())return;const t=()=>h(s(window.location.href)),e=t=>{const e=(t=>{if(!t)return null;const e=t.getAttribute("href");return e?/^https?:\/\//.test(e)||null!==t.getAttribute("target")?null:e:null})(t.target.closest("a"));e&&(t.preventDefault(),f(e))};return window.addEventListener("popstate",t),document.addEventListener("click",e),()=>{window.removeEventListener("popstate",t),document.removeEventListener("click",e)}},[f]),t.createElement(i.Provider,{value:[l,f]},o)};export{i as Context,l as Route,f as Switch,m as useHash,g as useParams,y as usePath,q as useQuery,h as useUrl};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossroad",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A React library to handle navigation in your WebApp. Built with simple components and React Hooks so your code is cleaner.",
5
5
  "homepage": "https://crossroad.page/",
6
6
  "repository": "https://github.com/franciscop/crossroad.git",
package/readme.md CHANGED
@@ -125,6 +125,30 @@ export default function App() {
125
125
 
126
126
  You would normally setup this Router straight on your App, along things like [Statux](https://statux.dev/)'s or [Redux](https://redux.js.org/)'s Store, error handling, translations, etc.
127
127
 
128
+ An example for a simple app:
129
+
130
+ ```js
131
+
132
+ // App.js
133
+ import Router, { Switch, Route} from "crossroad";
134
+
135
+ import Home from './pages/Home';
136
+ import Dashboard from './pages/Dashboard';
137
+ import Profile from './pages/Profile';
138
+
139
+ export default function App() {
140
+ return (
141
+ <Router>
142
+ <Switch>
143
+ <Route path="/" component={Home} />
144
+ <Route path="/dashboard" component={Dashboard} />
145
+ <Route path="/:username" component={Profile} />
146
+ </Switch>
147
+ </Router>
148
+ );
149
+ }
150
+ ```
151
+
128
152
  ### `<Switch />`
129
153
 
130
154
  A component that will only render the first of its children that matches the current URL. This is very useful to handle 404s, multiple routes matching, etc. For example, if you have a username system like `"/:username"` but want to have a help page, you can make it work easily with the switch:
@@ -356,11 +380,45 @@ setUrl({ ...url, path: "/" });
356
380
  setUrl({ ...url, query: { search: "hello" } });
357
381
 
358
382
  // Modify only one query param
359
- setUrl({ ...url, query: { ...url.query, safe: 0 } });
383
+ setUrl({ ...url, query: { ...url.query, safe: "no" } });
360
384
  ```
361
385
 
362
386
  `useUrl()` is powerful enough for all of your needs, but you might still be interested in other hooks to simplify situations where you do e.g. heavy query manipulation with `useQuery`.
363
387
 
388
+ #### Setter
389
+
390
+ The setter can be invoked directly, or with a callback:
391
+
392
+ ```js
393
+ setUrl('/newurl');
394
+ setUrl(oldUrl => '/newurl');
395
+ setUrl(oldUrl => ({ ...oldUrl, path: newPath }));
396
+ ```
397
+
398
+ The function `setUrl` is _always_ the same, so it doesn't matter whether you put it as a dependency or not. However the `path` can be updated and change, so you want to depend on it:
399
+
400
+ ```js
401
+ const [url, setUrl] = useurl();
402
+ useEffect(() => {
403
+ if (url.path === '/base') {
404
+ setUrl('/base/deeper');
405
+ }
406
+ }, [url, setUrl]);
407
+ ```
408
+
409
+ If you update the url with the current url, it won't trigger a rerender. So the above can also be written as this, removing all dependencies:
410
+
411
+ ```js
412
+ const [url, setUrl] = useUrl();
413
+ useEffect(() => {
414
+ setUrl(old => {
415
+ if (old.path === '/base') return '/base/deeper';
416
+ return old;
417
+ });
418
+ }, []);
419
+ ```
420
+
421
+
364
422
  #### New history entry
365
423
 
366
424
  By default `setUrl()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`:
@@ -369,7 +427,7 @@ By default `setUrl()` will create a new entry in the browser history. If you wan
369
427
  setUrl("/newurl", { mode: "replace" });
370
428
  ```
371
429
 
372
- - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/b?q=c` are both independent entries in your history.
430
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/c` are both independent entries in your history.
373
431
  - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/c` and then click on the back button, it'll go back to `/a`. This is because `/c` is overwriting `/b`, instead of adding a new entry.
374
432
 
375
433
  ### `usePath()`
@@ -389,8 +447,43 @@ const Login = () => {
389
447
  };
390
448
  ```
391
449
 
450
+ The path is always a string equivalent to `window.location.pathname`.
451
+
392
452
  > Note: this _only_ modifies the path(name) and keeps the search query and hash the same, so if you want to modify the full URL you should instead utilize `useUrl()` and `setUrl('/welcome')`
393
453
 
454
+ #### Setter
455
+
456
+ The setter can be invoked directly, or with a callback:
457
+
458
+ ```js
459
+ setPath('/newpath');
460
+ setPath(oldPath => '/newpath');
461
+ ```
462
+
463
+ The function `setPath` is _always_ the same, so it doesn't matter whether you put it as a dependency or not. However the `path` can be updated, so you might want to put that:
464
+
465
+ ```js
466
+ const [path, setPath] = usePath();
467
+ useEffect(() => {
468
+ if (path === '/base') {
469
+ setPath('/base/deeper');
470
+ }
471
+ }, [path, setPath]);
472
+ ```
473
+
474
+ If you update the path with the current path, it won't trigger a rerender. So the above can also be written as this, removing all dependencies:
475
+
476
+ ```js
477
+ const [path, setPath] = usePath();
478
+ useEffect(() => {
479
+ setPath(old => {
480
+ if (old === '/base') return '/base/deeper';
481
+ return old;
482
+ });
483
+ }, []);
484
+ ```
485
+
486
+
394
487
  #### New history entry
395
488
 
396
489
  By default `setPath()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`:
@@ -839,7 +932,7 @@ export default function Mock({ url, children }) {
839
932
  // Undo the setup when the component unmounts
840
933
  useEffect(() => {
841
934
  return () => Object.defineProperty(window, "location", oldLocation);
842
- });
935
+ }, []);
843
936
  return <div>{children}</div>;
844
937
  }
845
938
  ```