crossroad 1.1.4 → 1.2.1

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 +1 -1
  3. package/readme.md +60 -43
package/index.min.js CHANGED
@@ -1 +1 @@
1
- import t,{createContext as e,useState as r,useCallback as n,useEffect as o,useContext as a}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 o=e||"/";const a=new URLSearchParams(Object.entries(r).map(([t,e])=>(Array.isArray(e)?e:[e]).map(e=>[t,e])).flat().filter(([t,e])=>e)).toString();return a&&(o+="?"+a),n&&(o+="#"+n),o};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={},o=t.path.split("/").every((t,o)=>{const a=e.path.split("/")[o];return t.startsWith(":")?(n[t.slice(1)]=a,r):a===t});return o&&Object.assign(r,n),o&&r}var l=({path:e="*",exact:r=!0,component:n,render:o,children:s})=>{const p=a(i),u=c(e,p[0]);if(!u)return null;if(n){const e=n;s=t.createElement(e,u)}else if(o)s=o(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=a(i);if(!t)throw new Error("Wrap your App with <Router>");return t};var f=({redirect:t,children:e})=>{const[r,n]=h(),a=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>c(t.props.path||"*",r))||null;return o(()=>{t&&(a||("function"==typeof t&&(t=t(r)),n(p(t))))},[t,a]),a},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(),o=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],o]})(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]},w=t=>{const e=a(i);return t?c(t,e[0].path)||{}:e[0].params};export default({url:e,children:a})=>{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 o(()=>{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"));if(!e)return;t.preventDefault();const[r,n]=e.split("#");r&&f(r),n&&(window.location.hash="#"+n)};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]},a)};export{i as Context,l as Route,f as Switch,m as useHash,w as useParams,y as usePath,q as useQuery,h as useUrl};
1
+ import t,{createContext as e,useState as r,useCallback as n,useEffect as o,useContext as a}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 o=e||"/";const a=new URLSearchParams(Object.entries(r).map(([t,e])=>(Array.isArray(e)?e:[e]).map(e=>[t,e])).flat().filter(([t,e])=>e)).toString();return a&&(o+="?"+a),n&&(o+="#"+n),o};var u=()=>"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={},o=t.path.split("/").every((t,o)=>{const a=e.path.split("/")[o];return t.startsWith(":")?(n[t.slice(1)]=decodeURIComponent(a),r):a===t});return o&&Object.assign(r,n),o&&r}var c=({path:e="*",scrollUp:r,component:n,render:o,children:s})=>{const p=a(i),u=l(e,p[0]);if(!u)return null;if(r&&window.scrollTo(0,0),n){const e=n;s=t.createElement(e,u)}else if(o)s=o(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=a(i);if(!t)throw new Error("Wrap your App with <Router>");return t};var f=({redirect:t,children:e})=>{const[r,n]=h(),a=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>l(t.props.path||"*",r))||null;return o(()=>{t&&(a||("function"==typeof t&&(t=t(r)),n(p(t))))},[t,a]),a},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 w=t=>t?(t=>{const[e,r]=h(),o=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],o]})(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]},q=t=>{const e=a(i);return t?l(t,e[0].path)||{}:e[0].params};export default({scrollUp:e,url:a,children:l})=>{const c=a||(u()?"/":window.location.href),[h,f]=r(()=>s(c)),y=n((t,{mode:r="push"}={})=>{if(!history[r+"State"])throw new Error(`Invalid mode "${r}"`);t="function"==typeof t?t(h):t,f(n=>p(n)===p(t)?n:(history[r+"State"]({},null,p(t)),e&&window.scrollTo(0,0),s(t)))},[]);return o(()=>{if(u())return;const t=()=>f(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"));if(!e)return;t.preventDefault();const[r,n]=e.split("#");r&&y(r),n&&(window.location.hash="#"+n)};return window.addEventListener("popstate",t),document.addEventListener("click",e),()=>{window.removeEventListener("popstate",t),document.removeEventListener("click",e)}},[y]),t.createElement(i.Provider,{value:[h,y]},l)};export{i as Context,c as Route,f as Switch,m as useHash,q as useParams,y as usePath,w as useQuery,h as useUrl};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossroad",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
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
@@ -3,10 +3,11 @@
3
3
  A React library to handle navigation in your WebApp. Built with simple components and React Hooks so you write cleaner code:
4
4
 
5
5
  - `<Router>`, `<Switch>` and `<Route>` inspired by React Router so it's easy to get started.
6
- - Very useful hooks like [`useUrl`](#useurl), [`useQuery`](#usequery), etc.
6
+ - Very useful hooks like [`useUrl`](#useurl), [`useQuery`](#usequery), etc. Follow [the rules of hooks](https://reactjs.org/docs/hooks-rules.html).
7
7
  - Links are plain `<a>` instead of custom components. [Read more](#a).
8
8
  - The `<Route>` path is `exact` by default and can match query parameters.
9
9
  - It's [just ~1.5kb](https://bundlephobia.com/package/crossroad) (min+gzip) instead of the 17kb of React Router(+Dom).
10
+ - Add `scrollUp` to `<Router>` o `<Route>` to automatically scroll up on a route change.
10
11
 
11
12
  [**🔗 Demo on CodeSandbox**](https://codesandbox.io/s/recursing-wozniak-uftyo?file=/src/App.js)
12
13
 
@@ -123,18 +124,21 @@ export default function App() {
123
124
  }
124
125
  ```
125
126
 
127
+ Add the prop `scrollUp` to automatically scroll up the browser window when _any_ route changes. In contrast, you could also add it only to a single or multiple `<Route>`.
128
+
129
+ Add the prop `url` to simulate a fake URL instead of the current `window.location`, useful specially for testing.
130
+
126
131
  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
132
 
128
133
  An example for a simple app:
129
134
 
130
135
  ```js
131
-
132
136
  // App.js
133
- import Router, { Switch, Route} from "crossroad";
137
+ import Router, { Switch, Route } from "crossroad";
134
138
 
135
- import Home from './pages/Home';
136
- import Dashboard from './pages/Dashboard';
137
- import Profile from './pages/Profile';
139
+ import Home from "./pages/Home";
140
+ import Dashboard from "./pages/Dashboard";
141
+ import Profile from "./pages/Profile";
138
142
 
139
143
  export default function App() {
140
144
  return (
@@ -200,6 +204,7 @@ This component defines a conditional path that, when strictly matched, renders t
200
204
  - `component`: the component that will be rendered if the browser's URL matches the `path` parameter.
201
205
  - `render`: a function that will be called with the params if the browser's URL matches the `path` parameter.
202
206
  - `children`: the children to render if the browser's URL matches the `path` parameter.
207
+ - `scrollUp`: automatically scroll up the browser window when this route/component/etc is matched.
203
208
 
204
209
  So for example if the `path` prop is `"/user"` and you visit the page `"/user"`, then the component is rendered; it is ignored otherwise:
205
210
 
@@ -352,6 +357,10 @@ These are the structures of each:
352
357
  - `setUrl({ path: '/newpath', query: { hello: 'world' } })`: update the path and query (and delete the hash if any)
353
358
  - `setUrl(prev => ...)`: use the previous url (object)
354
359
 
360
+ `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`](#usequery).
361
+
362
+ #### url
363
+
355
364
  The resulting `url` is an object containing each of the parts of the URL:
356
365
 
357
366
  ```js
@@ -362,7 +371,25 @@ console.log(url.query); // { filter: hello }
362
371
  console.log(url.hash); // world
363
372
  ```
364
373
 
365
- You can also set it fully or partially:
374
+ It is memoized, so that if the url doesn't change then the object will remain the same. The same of course applies to the subelements like `url.path`. It will however change when the url changes, so you want to put it in your dependencies as usual:
375
+
376
+ ```js
377
+ // You can put the whole thing if you want to listen to
378
+ // ANY change on the url
379
+ useEffect(() => {
380
+ // ...
381
+ }, [url]);
382
+
383
+ // Or only a part of it. This is useful becase it WON'T trigger
384
+ // when the query or hashtag change
385
+ useEffect(() => {
386
+ // ...
387
+ }, [url.path]);
388
+ ```
389
+
390
+ #### Setter
391
+
392
+ The setter can be invoked directly, or with a callback:
366
393
 
367
394
  ```js
368
395
  const [url, setUrl] = useUrl();
@@ -383,27 +410,15 @@ setUrl({ ...url, query: { search: "hello" } });
383
410
  setUrl({ ...url, query: { ...url.query, safe: "no" } });
384
411
  ```
385
412
 
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`.
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
413
  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
414
 
400
415
  ```js
401
- const [url, setUrl] = useurl();
416
+ const [url, setUrl] = useUrl();
402
417
  useEffect(() => {
403
- if (url.path === '/base') {
404
- setUrl('/base/deeper');
418
+ if (url.path === "/base") {
419
+ setUrl("/base/deeper");
405
420
  }
406
- }, [url, setUrl]);
421
+ }, [url.path, setUrl]);
407
422
  ```
408
423
 
409
424
  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:
@@ -411,19 +426,19 @@ If you update the url with the current url, it won't trigger a rerender. So the
411
426
  ```js
412
427
  const [url, setUrl] = useUrl();
413
428
  useEffect(() => {
414
- setUrl(old => {
415
- if (old.path === '/base') return '/base/deeper';
429
+ setUrl((old) => {
430
+ if (old.path === "/base") return "/base/deeper";
416
431
  return old;
417
432
  });
418
433
  }, []);
419
434
  ```
420
435
 
421
-
422
436
  #### New history entry
423
437
 
424
438
  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' }`:
425
439
 
426
440
  ```js
441
+ setUrl("/newurl"); // Default: "push"
427
442
  setUrl("/newurl", { mode: "replace" });
428
443
  ```
429
444
 
@@ -447,17 +462,17 @@ const Login = () => {
447
462
  };
448
463
  ```
449
464
 
450
- The path is always a string equivalent to `window.location.pathname`.
465
+ The path is always a string equivalent to `window.location.pathname`. Why not use `window.location.pathname` then? Because usePath() is a hook that will trigger a re-render when the path changes!
451
466
 
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')`
467
+ > Note: `setPath` _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')`
453
468
 
454
469
  #### Setter
455
470
 
456
471
  The setter can be invoked directly, or with a callback:
457
472
 
458
473
  ```js
459
- setPath('/newpath');
460
- setPath(oldPath => '/newpath');
474
+ setPath("/newpath");
475
+ setPath((oldPath) => "/newpath");
461
476
  ```
462
477
 
463
478
  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:
@@ -465,8 +480,8 @@ The function `setPath` is _always_ the same, so it doesn't matter whether you pu
465
480
  ```js
466
481
  const [path, setPath] = usePath();
467
482
  useEffect(() => {
468
- if (path === '/base') {
469
- setPath('/base/deeper');
483
+ if (path === "/base") {
484
+ setPath("/base/deeper");
470
485
  }
471
486
  }, [path, setPath]);
472
487
  ```
@@ -476,19 +491,19 @@ If you update the path with the current path, it won't trigger a rerender. So th
476
491
  ```js
477
492
  const [path, setPath] = usePath();
478
493
  useEffect(() => {
479
- setPath(old => {
480
- if (old === '/base') return '/base/deeper';
494
+ setPath((old) => {
495
+ if (old === "/base") return "/base/deeper";
481
496
  return old;
482
497
  });
483
498
  }, []);
484
499
  ```
485
500
 
486
-
487
501
  #### New history entry
488
502
 
489
503
  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' }`:
490
504
 
491
505
  ```js
506
+ setPath("/newpath"); // Default: "push"
492
507
  setPath("/newpath", { mode: "replace" });
493
508
  ```
494
509
 
@@ -508,7 +523,7 @@ export default function SearchInput() {
508
523
  // [{ search: "" }, fn]
509
524
 
510
525
  // Goes to /users?search={value}
511
- const onChange = e => setQuery({ search: e.target.value });
526
+ const onChange = (e) => setQuery({ search: e.target.value });
512
527
 
513
528
  return <input value={query.search} onChange={onChange} />;
514
529
  }
@@ -537,7 +552,7 @@ setQuery({ search: "myname" });
537
552
  setQuery({ ...query, search: "myname" });
538
553
  // Goto /users?search=myname&filter=new
539
554
 
540
- setQuery(prev => ({ ...prev, search: "myname" }));
555
+ setQuery((prev) => ({ ...prev, search: "myname" }));
541
556
  // Goto /users?search=myname&filter=new
542
557
  ```
543
558
 
@@ -565,6 +580,7 @@ import { useQuery as useSearch } from 'crossroad';
565
580
  By default `setQuery()` will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with `{ mode: 'replace' }`:
566
581
 
567
582
  ```js
583
+ setQuery({ search: "abc" }); // Default: "push"
568
584
  setQuery({ search: "abc" }, { mode: "replace" });
569
585
  ```
570
586
 
@@ -597,6 +613,7 @@ If you want to remove the hash, pass a `null` or `undefined` to the setter.
597
613
  By default `setHash()` will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with `{ mode: 'replace' }`:
598
614
 
599
615
  ```js
616
+ setHash("newhash"); // Default: "push"
600
617
  setHash("newhash", { mode: "replace" });
601
618
  ```
602
619
 
@@ -679,7 +696,7 @@ That's it, in the [Codesandbox](https://codesandbox.io/s/loving-joana-jikne) we
679
696
 
680
697
  These refer to the websites where your username is straight after the domain, like Twitter (https://twitter.com/fpresencia). Of course Twitter has _other_ pages besides the username, so how can we emulate loading the page e.g. `/explore` in this case?
681
698
 
682
- The best way is to first define the known, company pages and then use the wildcard for the usernames:
699
+ The best way is to first define the known, company pages and then use the wildcard for the usernames. This **must** be inside a `<Switch>`, otherwise multiple will be rendered:
683
700
 
684
701
  ```js
685
702
  <Switch>
@@ -748,7 +765,7 @@ export default function SearchForm() {
748
765
  }
749
766
  ```
750
767
 
751
- In here we can see that we are treating the output of `useQuery` in the sam way that we'd treat the output of `useState()`. This is on purpose and it makes things a lot easier for your application to work.
768
+ In here we can see that we are treating the output of `useQuery` in the same way that we'd treat the output of `useState()`. This is on purpose and it makes things a lot easier for your application to work.
752
769
 
753
770
  ### Query routing
754
771
 
@@ -926,7 +943,7 @@ export default function Mock({ url, children }) {
926
943
  delete global.window.location;
927
944
  Object.defineProperty(global.window, "location", {
928
945
  value: new URL(href),
929
- configurable: true
946
+ configurable: true,
930
947
  });
931
948
 
932
949
  // Undo the setup when the component unmounts
@@ -991,10 +1008,10 @@ For Razzle (based on [these docs FaQ](https://razzlejs.org/docs/customization#tr
991
1008
  // razzle.config.js
992
1009
  module.exports = {
993
1010
  modifyWebpackOptions({ options: { webpackOptions } }) {
994
- webpackOptions.notNodeExternalResMatch = req => /crossroad/.test(req);
1011
+ webpackOptions.notNodeExternalResMatch = (req) => /crossroad/.test(req);
995
1012
  webpackOptions.babelRule.include.push(/crossroad/);
996
1013
  return webpackOptions;
997
- }
1014
+ },
998
1015
  };
999
1016
  ```
1000
1017
 
@@ -1053,7 +1070,7 @@ import { useUrl } from "crossroad";
1053
1070
 
1054
1071
  export default function LoginButton() {
1055
1072
  const [url, setUrl] = useUrl();
1056
- const login = async e => {
1073
+ const login = async (e) => {
1057
1074
  // ...
1058
1075
  setUrl("/welcome");
1059
1076
  };