crossroad 1.1.4 → 1.2.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 +1 -1
  3. package/readme.md +29 -26
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)]=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]})(),q=()=>{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]},m=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,q as useHash,m 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.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
@@ -7,6 +7,7 @@ A React library to handle navigation in your WebApp. Built with simple component
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
 
@@ -390,9 +395,9 @@ setUrl({ ...url, query: { ...url.query, safe: "no" } });
390
395
  The setter can be invoked directly, or with a callback:
391
396
 
392
397
  ```js
393
- setUrl('/newurl');
394
- setUrl(oldUrl => '/newurl');
395
- setUrl(oldUrl => ({ ...oldUrl, path: newPath }));
398
+ setUrl("/newurl");
399
+ setUrl((oldUrl) => "/newurl");
400
+ setUrl((oldUrl) => ({ ...oldUrl, path: newPath }));
396
401
  ```
397
402
 
398
403
  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:
@@ -400,8 +405,8 @@ The function `setUrl` is _always_ the same, so it doesn't matter whether you put
400
405
  ```js
401
406
  const [url, setUrl] = useurl();
402
407
  useEffect(() => {
403
- if (url.path === '/base') {
404
- setUrl('/base/deeper');
408
+ if (url.path === "/base") {
409
+ setUrl("/base/deeper");
405
410
  }
406
411
  }, [url, setUrl]);
407
412
  ```
@@ -411,14 +416,13 @@ If you update the url with the current url, it won't trigger a rerender. So the
411
416
  ```js
412
417
  const [url, setUrl] = useUrl();
413
418
  useEffect(() => {
414
- setUrl(old => {
415
- if (old.path === '/base') return '/base/deeper';
419
+ setUrl((old) => {
420
+ if (old.path === "/base") return "/base/deeper";
416
421
  return old;
417
422
  });
418
423
  }, []);
419
424
  ```
420
425
 
421
-
422
426
  #### New history entry
423
427
 
424
428
  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' }`:
@@ -456,8 +460,8 @@ The path is always a string equivalent to `window.location.pathname`.
456
460
  The setter can be invoked directly, or with a callback:
457
461
 
458
462
  ```js
459
- setPath('/newpath');
460
- setPath(oldPath => '/newpath');
463
+ setPath("/newpath");
464
+ setPath((oldPath) => "/newpath");
461
465
  ```
462
466
 
463
467
  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 +469,8 @@ The function `setPath` is _always_ the same, so it doesn't matter whether you pu
465
469
  ```js
466
470
  const [path, setPath] = usePath();
467
471
  useEffect(() => {
468
- if (path === '/base') {
469
- setPath('/base/deeper');
472
+ if (path === "/base") {
473
+ setPath("/base/deeper");
470
474
  }
471
475
  }, [path, setPath]);
472
476
  ```
@@ -476,14 +480,13 @@ If you update the path with the current path, it won't trigger a rerender. So th
476
480
  ```js
477
481
  const [path, setPath] = usePath();
478
482
  useEffect(() => {
479
- setPath(old => {
480
- if (old === '/base') return '/base/deeper';
483
+ setPath((old) => {
484
+ if (old === "/base") return "/base/deeper";
481
485
  return old;
482
486
  });
483
487
  }, []);
484
488
  ```
485
489
 
486
-
487
490
  #### New history entry
488
491
 
489
492
  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' }`:
@@ -508,7 +511,7 @@ export default function SearchInput() {
508
511
  // [{ search: "" }, fn]
509
512
 
510
513
  // Goes to /users?search={value}
511
- const onChange = e => setQuery({ search: e.target.value });
514
+ const onChange = (e) => setQuery({ search: e.target.value });
512
515
 
513
516
  return <input value={query.search} onChange={onChange} />;
514
517
  }
@@ -537,7 +540,7 @@ setQuery({ search: "myname" });
537
540
  setQuery({ ...query, search: "myname" });
538
541
  // Goto /users?search=myname&filter=new
539
542
 
540
- setQuery(prev => ({ ...prev, search: "myname" }));
543
+ setQuery((prev) => ({ ...prev, search: "myname" }));
541
544
  // Goto /users?search=myname&filter=new
542
545
  ```
543
546
 
@@ -926,7 +929,7 @@ export default function Mock({ url, children }) {
926
929
  delete global.window.location;
927
930
  Object.defineProperty(global.window, "location", {
928
931
  value: new URL(href),
929
- configurable: true
932
+ configurable: true,
930
933
  });
931
934
 
932
935
  // Undo the setup when the component unmounts
@@ -991,10 +994,10 @@ For Razzle (based on [these docs FaQ](https://razzlejs.org/docs/customization#tr
991
994
  // razzle.config.js
992
995
  module.exports = {
993
996
  modifyWebpackOptions({ options: { webpackOptions } }) {
994
- webpackOptions.notNodeExternalResMatch = req => /crossroad/.test(req);
997
+ webpackOptions.notNodeExternalResMatch = (req) => /crossroad/.test(req);
995
998
  webpackOptions.babelRule.include.push(/crossroad/);
996
999
  return webpackOptions;
997
- }
1000
+ },
998
1001
  };
999
1002
  ```
1000
1003
 
@@ -1053,7 +1056,7 @@ import { useUrl } from "crossroad";
1053
1056
 
1054
1057
  export default function LoginButton() {
1055
1058
  const [url, setUrl] = useUrl();
1056
- const login = async e => {
1059
+ const login = async (e) => {
1057
1060
  // ...
1058
1061
  setUrl("/welcome");
1059
1062
  };