crossroad 0.15.1 → 0.16.2
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 +113 -35
package/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t,{createContext as e,
|
|
1
|
+
import t,{createContext as e,useState as r,useEffect as n,useContext as a}from"react";var i=e();const o=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},s=t=>{if("string"==typeof t)return t;const{path:e,query:r={},hash:n}=t||{};let a=e||"/";const i=new URLSearchParams(Object.entries(r).filter(([t,e])=>e).map(([t,e])=>(Array.isArray(e)?e:[e]).map(e=>[t,e])).flat()).toString();return i&&(a+="?"+i),n&&(a+="#"+n),a};var l=()=>"undefined"==typeof window;function p(t,e,r={}){if(t=JSON.parse(JSON.stringify(o(t))),(e=JSON.parse(JSON.stringify(o(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 i=e.path.split("/")[a];return t.startsWith(":")?(n[t.slice(1)]=i,r):i===t});return a&&Object.assign(r,n),a&&r}var h=({path:e="*",exact:r=!0,component:n,render:o,children:s})=>{const l=a(i),h=p(e,l[0]);if(!h)return null;if(n){const e=n;s=t.createElement(e,h)}else if(o)s=o(h);else if(!s)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(i.Provider,{value:[{...l[0],params:h},...l.slice(1)]},s)},c=()=>{const t=a(i);if(!t)throw new Error("Wrap your App with <Router>");return t};var u=({redirect:t,children:e})=>{const[r,a]=c(),i=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>p(t.props.path||"*",r))||null;return n(()=>{t&&(i||("function"==typeof t&&(t=t(r)),a(s(t))))},[t,i]),i},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)=>{if(null===e){const{[t]:e,...i}=n;return a(i,r)}return 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(i);return t?p(t,e[0].path)||{}:e[0].params};export default({url:e,children:a})=>{const p=e||(l()?"/":window.location.href),[h,c]=r(()=>o(p)),u=(t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);history[e+"State"]({},null,s(t)),c(o(t))};return n(()=>{if(l())return;const t=()=>c(o(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(i.Provider,{value:[h,u]},a)};export{i as Context,h as Route,u as Switch,y as useHash,m as useParams,f as usePath,d as useQuery,c as useUrl};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crossroad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.2",
|
|
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
|
@@ -302,6 +302,8 @@ Some examples:
|
|
|
302
302
|
Read and set the full URL:
|
|
303
303
|
|
|
304
304
|
```js
|
|
305
|
+
import { useUrl } from "crossroad";
|
|
306
|
+
|
|
305
307
|
export default function Login() {
|
|
306
308
|
const [url, setUrl] = useUrl();
|
|
307
309
|
|
|
@@ -341,21 +343,35 @@ You can also set it fully or partially:
|
|
|
341
343
|
```js
|
|
342
344
|
const [url, setUrl] = useUrl();
|
|
343
345
|
|
|
344
|
-
|
|
345
|
-
setUrl(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
setUrl({
|
|
346
|
+
// [Shorthand] Redirect to home with a hashtag
|
|
347
|
+
setUrl("/#firsttime");
|
|
348
|
+
|
|
349
|
+
// Same as above, but specifying the parts
|
|
350
|
+
setUrl({ path: "/", hash: "firsttime" });
|
|
351
|
+
|
|
352
|
+
// Keep everything the same except the path
|
|
353
|
+
setUrl({ ...url, path: "/" });
|
|
354
|
+
|
|
355
|
+
// Set a full search query
|
|
356
|
+
setUrl({ ...url, query: { search: "hello" } });
|
|
357
|
+
|
|
358
|
+
// Modify only one query param
|
|
359
|
+
setUrl({ ...url, query: { ...url.query, safe: 0 } });
|
|
349
360
|
```
|
|
350
361
|
|
|
351
362
|
`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`.
|
|
352
363
|
|
|
353
|
-
|
|
364
|
+
#### New history entry
|
|
365
|
+
|
|
366
|
+
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' }`:
|
|
354
367
|
|
|
355
368
|
```js
|
|
356
369
|
setUrl("/newurl", { mode: "replace" });
|
|
357
370
|
```
|
|
358
371
|
|
|
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`. This is because `/b` and `/b?q=c` are both independent entries in your history.
|
|
373
|
+
- `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
|
+
|
|
359
375
|
### `usePath()`
|
|
360
376
|
|
|
361
377
|
Read and set only the path(name) part of the URL:
|
|
@@ -375,36 +391,44 @@ const Login = () => {
|
|
|
375
391
|
|
|
376
392
|
> 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')`
|
|
377
393
|
|
|
378
|
-
|
|
394
|
+
#### New history entry
|
|
395
|
+
|
|
396
|
+
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' }`:
|
|
379
397
|
|
|
380
398
|
```js
|
|
381
|
-
setPath("/
|
|
399
|
+
setPath("/newpath", { mode: "replace" });
|
|
382
400
|
```
|
|
383
401
|
|
|
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`. This is because `/b` and `/b?q=c` are both independent entries in your history.
|
|
403
|
+
- `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
|
+
|
|
384
405
|
### `useQuery()`
|
|
385
406
|
|
|
386
407
|
Read and set only the search query parameters from the URL:
|
|
387
408
|
|
|
388
409
|
```js
|
|
389
|
-
|
|
390
|
-
const [query, setQuery] = useQuery();
|
|
391
|
-
// { search: 'name', filter: 'new' }
|
|
410
|
+
import { useQuery } from "crossroad";
|
|
392
411
|
|
|
393
|
-
|
|
394
|
-
//
|
|
412
|
+
export default function SearchInput() {
|
|
413
|
+
// In /users?search=
|
|
414
|
+
const [query, setQuery] = useQuery();
|
|
415
|
+
// [{ search: "" }, fn]
|
|
395
416
|
|
|
396
|
-
|
|
397
|
-
|
|
417
|
+
// Goes to /users?search={value}
|
|
418
|
+
const onChange = e => setQuery({ search: e.target.value });
|
|
419
|
+
|
|
420
|
+
return <input value={query.search} onChange={onChange} />;
|
|
421
|
+
}
|
|
398
422
|
```
|
|
399
423
|
|
|
400
|
-
If you pass a
|
|
424
|
+
If you pass a key, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form:
|
|
401
425
|
|
|
402
426
|
```js
|
|
403
427
|
// In /users?search=name&filter=new
|
|
404
428
|
const [search, setSearch] = useQuery("search");
|
|
405
429
|
// 'name'
|
|
406
430
|
|
|
407
|
-
|
|
431
|
+
setSearch("myname");
|
|
408
432
|
// Goto /users?search=myname&filter=new
|
|
409
433
|
```
|
|
410
434
|
|
|
@@ -414,27 +438,46 @@ When you update it, it will clean any parameter not passed, so make sure to pass
|
|
|
414
438
|
// In /users?search=name&filter=new
|
|
415
439
|
const [query, setQuery] = useQuery();
|
|
416
440
|
|
|
417
|
-
setQuery({ search: "myname" });
|
|
418
|
-
|
|
419
|
-
|
|
441
|
+
setQuery({ search: "myname" });
|
|
442
|
+
// Goto /users?search=myname (removes the filter)
|
|
443
|
+
|
|
444
|
+
setQuery({ ...query, search: "myname" });
|
|
445
|
+
// Goto /users?search=myname&filter=new
|
|
446
|
+
|
|
447
|
+
setQuery(prev => ({ ...prev, search: "myname" }));
|
|
448
|
+
// Goto /users?search=myname&filter=new
|
|
420
449
|
```
|
|
421
450
|
|
|
422
451
|
`setQuery` only modifies the query string part of the URL, keeping the `path` and `hash` the same as they were previously.
|
|
423
452
|
|
|
424
|
-
|
|
453
|
+
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:
|
|
425
454
|
|
|
426
455
|
```js
|
|
427
|
-
|
|
456
|
+
const [myname, setMyname] = useQuery("myname");
|
|
457
|
+
|
|
458
|
+
// ...
|
|
459
|
+
|
|
460
|
+
setMyname(newName || null);
|
|
428
461
|
```
|
|
429
462
|
|
|
430
|
-
|
|
463
|
+
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:
|
|
431
464
|
|
|
432
465
|
```js
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
setWord(newName || null);
|
|
466
|
+
import { useQuery as useSearch } from 'crossroad';
|
|
467
|
+
...
|
|
436
468
|
```
|
|
437
469
|
|
|
470
|
+
#### New history entry
|
|
471
|
+
|
|
472
|
+
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' }`:
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
setQuery({ search: "abc" }, { mode: "replace" });
|
|
476
|
+
```
|
|
477
|
+
|
|
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`. This is because `/b` and `/b?q=c` are both independent entries in your history.
|
|
479
|
+
- `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
|
+
|
|
438
481
|
### `useHash()`
|
|
439
482
|
|
|
440
483
|
Read and set only the hash part of the URL (without the `"#"`):
|
|
@@ -454,6 +497,19 @@ By default `setHash()` will create a new entry in the browser history. If you wa
|
|
|
454
497
|
setHash("newhash", { mode: "replace" });
|
|
455
498
|
```
|
|
456
499
|
|
|
500
|
+
If you want to remove the hash, pass a `null` or `undefined` to the setter.
|
|
501
|
+
|
|
502
|
+
#### New history entry
|
|
503
|
+
|
|
504
|
+
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' }`:
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
setHash("newhash", { mode: "replace" });
|
|
508
|
+
```
|
|
509
|
+
|
|
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`. This is because `/b` and `/b?q=c` are both independent entries in your history.
|
|
511
|
+
- `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
|
+
|
|
457
513
|
### `useParams()`
|
|
458
514
|
|
|
459
515
|
Parse the current URL against the given reference:
|
|
@@ -464,6 +520,8 @@ const params = useParams("/users/:id");
|
|
|
464
520
|
// { id: '2' }
|
|
465
521
|
```
|
|
466
522
|
|
|
523
|
+
> Note: this returns a plain object, not a [value, setter] array
|
|
524
|
+
|
|
467
525
|
It's not this method responsibility to match the url, just to attempt to parse it, so if there's no good match it'll just return an empty object (use a `<Route />` for path matching):
|
|
468
526
|
|
|
469
527
|
```js
|
|
@@ -667,9 +725,9 @@ In this case the order matters, because the generic NotFound will be matched wit
|
|
|
667
725
|
|
|
668
726
|
> NOTE: this is a bad idea for SEO, but if that doesn't matter much for you go ahead and host your webapp in Github Pages
|
|
669
727
|
|
|
670
|
-
Github pages is a bit particular in that as of this writing it does not allow for a generic redirect like most other static website servers, so we need to do a workaround with the `
|
|
728
|
+
Github pages is a bit particular in that as of this writing it does not allow for a generic redirect like most other static website servers, so we need to do a workaround with the `404.html` page.
|
|
671
729
|
|
|
672
|
-
This is because any of your visitors landing on `https://example.com/` will see the proper website (since that'll be directed to `docs/index.html`), but when the user lands on other paths like `https://example.com/info` it'll not find `docs/info.html` and thus render `
|
|
730
|
+
This is because any of your visitors landing on `https://example.com/` will see the proper website (since that'll be directed to `docs/index.html`), but when the user lands on other paths like `https://example.com/info` it'll not find `docs/info.html` and thus render `404.html`.
|
|
673
731
|
|
|
674
732
|
So let's save the url and setup a redirect in `404.html`:
|
|
675
733
|
|
|
@@ -730,7 +788,7 @@ export default function App({ url }) {
|
|
|
730
788
|
}
|
|
731
789
|
```
|
|
732
790
|
|
|
733
|
-
|
|
791
|
+
How does it work? The `url` prop will be undefined when a user loads the app (since we **don't** add it to index.js), so it is only being written for testing. On the users' browser, since it's undefinde Crossroad will use `window.location.href` instead.
|
|
734
792
|
|
|
735
793
|
```js
|
|
736
794
|
// App.test.js
|
|
@@ -759,6 +817,10 @@ describe("use the url prop", () => {
|
|
|
759
817
|
|
|
760
818
|
This method is the simplest to get started, but some people don't like having to add code to the production website only for the testing environment. That's all fine, there's another way that is a bit harder to setup but it's also more accurate to the browser's real behavior.
|
|
761
819
|
|
|
820
|
+
#### Mock window.location
|
|
821
|
+
|
|
822
|
+
The previous method has **a big limitation**: it doesn't allow you to navigate within your app for a test, since it's always forcing the same url. To avoid this and be able to test better the real-world behavior, use this method.
|
|
823
|
+
|
|
762
824
|
When you are running Jest, it creates a fake `window` already, so you can plug into that to mock the behavior for the duration of the test. Doing it with a React component makes it even smoother:
|
|
763
825
|
|
|
764
826
|
```js
|
|
@@ -823,7 +885,27 @@ describe("use the Mock component", () => {
|
|
|
823
885
|
|
|
824
886
|
### Server Side Render
|
|
825
887
|
|
|
826
|
-
Crossroad has been tested with
|
|
888
|
+
Crossroad has been tested with these libraries/frameworks for SSR:
|
|
889
|
+
|
|
890
|
+
- ✅ [Razzle](https://razzlejs.org/): it works adding a bit of config; Razzle bundles React Router Dom by default, so you need to install Crossroad, remove React Router Dom and add the code mentioned below.
|
|
891
|
+
- ⚠️ [Next.js](https://nextjs.org/): it works, but is generally not needed since Next.js include its own router and file-based routing.
|
|
892
|
+
- ❌ [Babel-Node](https://babeljs.io/docs/en/babel-node): BabelNode [doesn't support ECMAScript modules (ESM)](https://babeljs.io/docs/en/babel-node#es6-style-module-loading-may-not-function-as-expected), but you are **also** [not supposed to use `babel-node` for production anyway](https://babeljs.io/docs/en/babel-node#not-meant-for-production-use) so this is not a real framework for SSR.
|
|
893
|
+
- Others? I couldn't find many other ways that people are running SSR that I could test.
|
|
894
|
+
|
|
895
|
+
For Razzle (based on [these docs FaQ](https://razzlejs.org/docs/customization#transpilation-of-external-modules)):
|
|
896
|
+
|
|
897
|
+
```js
|
|
898
|
+
// razzle.config.js
|
|
899
|
+
module.exports = {
|
|
900
|
+
modifyWebpackOptions({ options: { webpackOptions } }) {
|
|
901
|
+
webpackOptions.notNodeExternalResMatch = req => /crossroad/.test(req);
|
|
902
|
+
webpackOptions.babelRule.include.push(/crossroad/);
|
|
903
|
+
return webpackOptions;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
When working on the server, and similar to [how we saw in testing](#testing-routes), we can overload the current url:
|
|
827
909
|
|
|
828
910
|
```js
|
|
829
911
|
// An express example
|
|
@@ -834,16 +916,12 @@ app.get("/users", (req, res) => {
|
|
|
834
916
|
});
|
|
835
917
|
|
|
836
918
|
app.get("/users/:id", (req, res) => {
|
|
837
|
-
const id = req.params.id;
|
|
838
|
-
|
|
839
919
|
// {...} validate the `id` it here!
|
|
840
920
|
|
|
841
|
-
res.render(<App url={
|
|
921
|
+
res.render(<App url={req.url} />);
|
|
842
922
|
});
|
|
843
923
|
```
|
|
844
924
|
|
|
845
|
-
There is a big warning in `babel-node` and that applies to us as well. Babel-node [doesn't work with proper EcmaScript Modules (ESM)](https://babeljs.io/docs/en/babel-node#es6-style-module-loading-may-not-function-as-expected) in libraries, so if you are using `babel-node` to compile your Node.js code from JSX to JS, it'll not work with Crossroad. `babel-node` is also [not supposed to be used in production](https://babeljs.io/docs/en/babel-node#not-meant-for-production-use) anyway, so it should not be a big deal.
|
|
846
|
-
|
|
847
925
|
## React Router diff
|
|
848
926
|
|
|
849
927
|
This part of the documentation tries to explain in detail the differences between Crossroad and React Router (Dom). Crossroad goal is to build a modern Router API from scratch, removing the legacy code and using Hooks natively.
|