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.
- package/index.min.js +1 -1
- package/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
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] =
|
|
416
|
+
const [url, setUrl] = useUrl();
|
|
402
417
|
useEffect(() => {
|
|
403
|
-
if (url.path ===
|
|
404
|
-
setUrl(
|
|
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 ===
|
|
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:
|
|
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(
|
|
460
|
-
setPath(oldPath =>
|
|
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 ===
|
|
469
|
-
setPath(
|
|
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 ===
|
|
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
|
|
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
|
};
|