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.
- package/index.min.js +1 -1
- package/package.json +1 -1
- 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
|
|
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.
|
|
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
|
|
136
|
-
import Dashboard from
|
|
137
|
-
import Profile from
|
|
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(
|
|
394
|
-
setUrl(oldUrl =>
|
|
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 ===
|
|
404
|
-
setUrl(
|
|
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 ===
|
|
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(
|
|
460
|
-
setPath(oldPath =>
|
|
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 ===
|
|
469
|
-
setPath(
|
|
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 ===
|
|
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
|
};
|