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.
Files changed (3) hide show
  1. package/index.min.js +1 -1
  2. package/package.json +1 -1
  3. package/readme.md +113 -35
package/index.min.js CHANGED
@@ -1 +1 @@
1
- import t,{createContext as e,useContext as r,useState as n,useEffect as a}from"react";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},i=t=>{if("string"==typeof t)return t;const{path:e,query:r={},hash:n}=t||{};let a=e||"/";const s=new URLSearchParams(Object.entries(r).map(([t,e])=>(e.map?e:[e]).map(e=>[t,e])).flat()).toString();return s&&(a+="?"+s),n&&(a+="#"+n),a};function o(t,e,r={}){if(t=JSON.parse(JSON.stringify(s(t))),(e=JSON.parse(JSON.stringify(s(e)))).path.endsWith("/")&&(e.path=e.path.slice(0,-1)||"/"),t.path.endsWith("/")&&(t.path=t.path.slice(0,-1)||"/"),t.path.endsWith("*")){t.path=t.path.replace(/\/?\*/,"")||"/";const r=t.path.slice(1).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(e.path===t.path)return r;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 s=e.path.split("/")[a];return t.startsWith(":")?(n[t.slice(1)]=s,r):s===t});return a&&Object.assign(r,n),a&&r}var p=e();const h=()=>{const t=r(p);if(!t)throw new Error("Wrap your App with <Router>");return t},l=()=>{const[t,e]=h();return[t.path,(r,n)=>e({...t,path:r},n)]},c=t=>{const[e,r]=h(),n=e.query,a=(t,n)=>r({...e,query:t},n);return t?[n[t],(e,r)=>a({...n,[t]:e},r)]:[n,a]},u=()=>{const[t,e]=h();return[t.hash,(r,n)=>e({...t,hash:r},n)]},f=t=>{const e=r(p),[n]=l();if(t){const e={};return o(t,n,e),e}return e[0].params},d=()=>"undefined"==typeof window,y=({path:e="*",exact:n=!0,component:a,render:s,children:i})=>{const h=r(p),l=o(e,h[0]);if(!l)return null;if(a){const e=a;i=t.createElement(e,l)}else if(s)i=s(l);else if(!i)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(p.Provider,{value:[{...h[0],params:l},...h.slice(1)]},i)},m=({redirect:t,children:e})=>{const[r,n]=h(),s=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>o(t.props.path||"*",r))||null;return a(()=>{t&&(s||("function"==typeof t&&(t=t(r)),n(i(t))))},[t,s]),s};export default({url:e,children:r})=>{const o=e||(d()?"/":window.location.href),[h,l]=n(()=>s(o)),c=(t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);history[e+"State"]({},null,i(t)),l(s(t))};return a(()=>{if(d())return;const t=()=>c(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(),c(e))};return window.addEventListener("popstate",t),document.addEventListener("click",e),()=>{window.removeEventListener("popstate",t),document.removeEventListener("click",e)}},[]),t.createElement(p.Provider,{value:[h,c]},r)};export{p as Context,y as Route,m as Switch,u as useHash,f as useParams,l as usePath,c as useQuery,h as useUrl};
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.15.1",
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
- setUrl("/#firsttime"); // [Shorthand] Redirect to home with a hashtag
345
- setUrl({ path: "/", hash: "firsttime" }); // Same as above
346
- setUrl({ ...url, path: "/" }); // Keep everything the same except the path
347
- setUrl({ ...url, query: { search: myQuery } }); // Set a full search query
348
- setUrl({ ...url, query: { ...url.query, safe: 0 } }); // Modify only one query param
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
- By default `setUrl()` will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with `{ mode: 'replace' }`:
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
- By default `setPath()` will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with `{ mode: 'replace' }`:
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("/newurl", { mode: "replace" });
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
- // In /users?search=name&filter=new
390
- const [query, setQuery] = useQuery();
391
- // { search: 'name', filter: 'new' }
410
+ import { useQuery } from "crossroad";
392
411
 
393
- setQuery({ search: "myname" }); // Remove the other query params
394
- // Goto /users?search=myname
412
+ export default function SearchInput() {
413
+ // In /users?search=
414
+ const [query, setQuery] = useQuery();
415
+ // [{ search: "" }, fn]
395
416
 
396
- setQuery({ ...query, search: "myname" }); // Keep the other query params
397
- // Goto /users?search=myname&filter=new
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 parameter, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form:
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
- setQuery("myname");
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" }); // Goto /users?q=myname (removes the filter)
418
- setQuery({ ...query, search: "myname" }); // Goto /users?q=myname&filter=new
419
- setQuery(prev => ({ ...prev, search: "myname" })); // Goto /users?q=myname&filter=new
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
- 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' }`:
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
- setQuery({ search: "abc" }, { mode: "replace" });
456
+ const [myname, setMyname] = useQuery("myname");
457
+
458
+ // ...
459
+
460
+ setMyname(newName || null);
428
461
  ```
429
462
 
430
- When you set a search query to `null` or `false`, it will be removed from the URL. However, empty strings are not removed. So if you want empty strings to also remove the parameter in the URL, please do this:
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
- const [myname, setMyname] = useQuery("myname");
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 `notfound.html` page.
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 `nofound.html`.
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
- Now let's see how to test it. The `url` prop will be undefined in the browser, so it'll use `window.location.href`, so it'll only apply to testing:
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 Next.js and should work both on the server as in the browser. When working on the server, and similar to [how we saw in testing](#testing-routes), we can overload the current url:
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={`/users/${id}`} />);
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.