crossroad 0.16.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.
Files changed (3) hide show
  1. package/index.min.js +1 -1
  2. package/package.json +2 -2
  3. package/readme.md +104 -11
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 o=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},i=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])=>(e.map?e:[e]).map(e=>[t,e])).flat()).toString();return o&&(a+="?"+o),n&&(a+="#"+n),a};var p=()=>"undefined"==typeof window;function l(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 h=({path:e="*",exact:r=!0,component:n,render:s,children:i})=>{const p=a(o),h=l(e,p[0]);if(!h)return null;if(n){const e=n;i=t.createElement(e,h)}else if(s)i=s(h);else if(!i)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(o.Provider,{value:[{...p[0],params:h},...p.slice(1)]},i)},c=()=>{const t=a(o);if(!t)throw new Error("Wrap your App with <Router>");return t};var u=({redirect:t,children:e})=>{const[r,a]=c(),o=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>l(t.props.path||"*",r))||null;return n(()=>{t&&(o||("function"==typeof t&&(t=t(r)),a(i(t))))},[t,o]),o},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)=>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(o);return t?l(t,e[0].path)||{}:e[0].params};export default({url:e,children:a})=>{const l=e||(p()?"/":window.location.href),[h,c]=r(()=>s(l)),u=(t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);history[e+"State"]({},null,i(t)),c(s(t))};return n(()=>{if(p())return;const t=()=>u(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(o.Provider,{value:[h,u]},a)};export{o 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": "0.16.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",
@@ -35,7 +35,7 @@
35
35
  "babel-polyfill": "^6.26.0",
36
36
  "jest": "^25.3.0",
37
37
  "react": "^16.8.0 || ^17.0.0",
38
- "react-test": "^0.9.1",
38
+ "react-test": "^0.10.2",
39
39
  "rollup": "^1.32.1",
40
40
  "rollup-plugin-babel": "^4.4.0",
41
41
  "rollup-plugin-terser": "^5.2.0"
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`.
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' }`:
@@ -399,7 +492,7 @@ By default `setPath()` will create a new entry in the browser history. If you wa
399
492
  setPath("/newpath", { mode: "replace" });
400
493
  ```
401
494
 
402
- - `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`.
495
+ - `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.
403
496
  - `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.
404
497
 
405
498
  ### `useQuery()`
@@ -439,18 +532,18 @@ When you update it, it will clean any parameter not passed, so make sure to pass
439
532
  const [query, setQuery] = useQuery();
440
533
 
441
534
  setQuery({ search: "myname" });
442
- // Goto /users?q=myname (removes the filter)
535
+ // Goto /users?search=myname (removes the filter)
443
536
 
444
537
  setQuery({ ...query, search: "myname" });
445
- // Goto /users?q=myname&filter=new
538
+ // Goto /users?search=myname&filter=new
446
539
 
447
540
  setQuery(prev => ({ ...prev, search: "myname" }));
448
- // Goto /users?q=myname&filter=new
541
+ // Goto /users?search=myname&filter=new
449
542
  ```
450
543
 
451
544
  `setQuery` only modifies the query string part of the URL, keeping the `path` and `hash` the same as they were previously.
452
545
 
453
- When you set a search query to `null` or `false`, it will be removed from the URL. However, empty strings are not removed. So if you want empty strings to also remove the parameter in the URL, please do this:
546
+ When you set a search query to `null` it will be removed from the URL. However, empty strings `""`, zero `0` or boolean `false` are not removed. So if you want falsy values to also remove the parameter in the URL, please do this:
454
547
 
455
548
  ```js
456
549
  const [myname, setMyname] = useQuery("myname");
@@ -460,7 +553,7 @@ const [myname, setMyname] = useQuery("myname");
460
553
  setMyname(newName || null);
461
554
  ```
462
555
 
463
- If you are using `react-query` and already have a bunch of `useQuery()` in your code and prefer to use other name, just rename this method when importing it:
556
+ If you are using `react-query` and already have a bunch of `useQuery()` in your code and prefer to use other name, you can rename this method when importing it:
464
557
 
465
558
  ```js
466
559
  import { useQuery as useSearch } from 'crossroad';
@@ -475,7 +568,7 @@ By default `setQuery()` will create a new entry in the browser history. If you w
475
568
  setQuery({ search: "abc" }, { mode: "replace" });
476
569
  ```
477
570
 
478
- - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b?q=c` and then click on the back button, the browser will go back to `/b`.
571
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b?q=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.
479
572
  - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b?q=c` and then click on the back button, it'll go back to `/a`. This is because `/b?q=c` is overwriting `/b`, instead of adding a new entry.
480
573
 
481
574
  ### `useHash()`
@@ -507,7 +600,7 @@ By default `setHash()` will create a new entry in the browser history. If you wa
507
600
  setHash("newhash", { mode: "replace" });
508
601
  ```
509
602
 
510
- - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b#c` and then click on the back button, the browser will go back to `/b`.
603
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b#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.
511
604
  - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b#c` and then click on the back button, it'll go back to `/a`. This is because `/b#c` is overwriting `/b`, instead of adding a new entry.
512
605
 
513
606
  ### `useParams()`
@@ -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
  ```