@fruit-ui/router 1.0.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +39 -7
  2. package/package.json +1 -1
  3. package/src/router.js +69 -20
package/README.md CHANGED
@@ -1,11 +1,43 @@
1
- ## FRUIT Router
1
+ # FRUIT Router
2
2
 
3
- This is a basic router component for [FRUIT](https://www.npmjs.com/package/@fruit-ui/core). It uses search params for routes, which minimizes server-side requirements. Its basic principles can be extended to RegExp-matched routes and could be applied to create a path-based router.
3
+ This is a basic router component for [FRUIT](https://www.npmjs.com/package/@fruit-ui/core). It uses search params for routes, which minimizes server-side requirements.
4
4
 
5
- This router provides four components:
6
- - the Router component. This takes in `routes`, an object mapping from paths (strings) to Routes, objects with a `route` method to generate the appropriate Template, Component etc. It also allows one 'wildcard' (*) route, whose function can take in the name of the route as a parameter. The Route can also have a `title`, which will set the window's title for that route.
7
- - the `navigate` function, which takes in a path and navigates to that path. Navigation is done with `history.pushState` so it is compatible with the browser forward/back methods.
8
- - the `pagechange` event. Elements with `data-receive-page-changes` enabled will receive the `pagechange` event which can be listened for like any other event. This event is broadcast when the `navigate` function is called or forward/back methods are used in the browser. (It is *not*, however, broadcast on page load.) Within the listener, the new route can be accessed using `event.detail.page`.
9
- - the `getPage` function, which returns the current path.
5
+ This router provides four significant pieces:
6
+
7
+ ## The Router component
8
+
9
+ This is a component producer which takes in three props.
10
+ - `routes` is an object mapping from paths (strings) to `Route`s. `Route`s are objects with a `route` method (which generates the template/component for that route, and is allowed to be asynchronous.) You can also have a 'wildcard' (*) route whose `route` method can take in the name of the current route as a prop. Each `Route` can also have an attribute `title` which will set the window's title for that path.
11
+ - `scrollOptions` is an optional prop. It is an object describing how scrolling should be handled when the Router component rerenders. It has two attributes: `hashed` and `unhashed`. These respectively describe how scrolling should be handled given hashed (`?page=about#contact`) and unhashed (`?page=about`) paths. In each of these, you may select any options from the [scrollIntoView](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) method, as well as an option `to: {x: number, y: number}`, which can take coordinates (in which case `scrollTo` will be used rather than `scrollIntoView`). Either can also be `false` in which case no scrolling will occur. The default value is `{hashed: {}, unhashed: {to: {x: 0, y: 0}}}`.
12
+ - `asyncLoader` is an optional prop describing what should be displayed while an asynchronous route is loading. This is only used on the first route visited in a session; in subsequent routes, the router displays the old route until the new async route has fully loaded. The default value is `{}` (an empty `div`).
13
+
14
+ Here is an example router:
15
+
16
+ ```js
17
+ import * as router from "@fruit-ui/router";
18
+
19
+ const router = router.Router({
20
+ '': () => HomePage,
21
+ 'about': () => AboutPage,
22
+ 'contact': () => ContactPage,
23
+ '*': (page) => ({children: [{tag: 'h2', children: '404'}, {tag: 'p', children: `The page "${page}" does not exist.`}]})
24
+ }, {
25
+ hashed: {behavior: 'smooth'},
26
+ unhashed: {behavior: 'smooth', to: {x: 0, y: 0}}
27
+ })
28
+ ```
29
+
30
+ ## The `navigate` function
31
+
32
+ The `navigate` function takes in a path and navigates to that path. Navigation is done with `history.pushState` so it is compatible with the browser forward/back methods. You can navigate to hashed paths (i.e., `navigate('/about#contact')`) to automatically scroll to a certain element ID, depending on scroll settings.
33
+
34
+ ## The `pagechange` event
35
+
36
+ Elements with `data-receive-page-changes` enabled will receive the `pagechange` event which can be listened for like any other event. This event is broadcast when the `navigate` function is called or forward/back methods are used in the browser. (It is *not*, however, broadcast on page load.) Within the listener, the new route can be accessed using `event.detail.page`.
37
+
38
+ This attribute can be given using `dataset: {receivePageChanges: true}`.
39
+
40
+ ## The `getPage` function
41
+ This returns the current path. Note that hashes and opening slashes are automatically removed, so after `navigate('/about#contact')`, a `getPage()` call will return `'about'`.
10
42
 
11
43
  More thorough documentation for FRUIT Router will be available shortly [here](https://asantagata.github.io/fruit-ui/?page=router-index).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruit-ui/router",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A basic router component for FRUIT",
5
5
  "main": "src/router.js",
6
6
  "homepage": "https://asantagata.github.io/fruit-ui/?page=router-index",
package/src/router.js CHANGED
@@ -1,5 +1,3 @@
1
- import { replaceWith } from "https://cdn.jsdelivr.net/npm/@fruit-ui/core@latest/src/index.js";
2
-
3
1
  const PARAM_NAME = 'page';
4
2
 
5
3
  /**
@@ -25,28 +23,63 @@ function getRoute(routes, page) {
25
23
  return {};
26
24
  }
27
25
 
26
+ /**
27
+ * Custom addon to scrollIntoView with coords handling.
28
+ * @param {HTMLElement} target
29
+ * @param {object} scrollOptions
30
+ */
31
+ function scrollIntoView(target, scrollOptions) {
32
+ if (scrollOptions.to) {
33
+ target.scrollTo({...scrollOptions, top: scrollOptions.to.y, left: scrollOptions.to.x});
34
+ } else {
35
+ target.scrollIntoView(scrollOptions);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Handles scrolling.
41
+ * @param {HTMLElement} element - Router element
42
+ * @param {object | false} scrollOptions - Scroll options
43
+ */
44
+ function handleScrolling(element, scrollOptions) {
45
+ if (window.location.hash && document.getElementById(window.location.hash.slice(1))) {
46
+ if (scrollOptions.hashed)
47
+ scrollIntoView(document.getElementById(window.location.hash.slice(1)), scrollOptions.hashed);
48
+ } else {
49
+ if (scrollOptions.unhashed)
50
+ scrollIntoView(element, scrollOptions.unhashed);
51
+ }
52
+ }
53
+
28
54
  /**
29
55
  * A basic router component.
30
56
  * @param {Object.<string, {route: () => object, title?: string}>} routes - the collection of routes.
57
+ * @param {object | false} [scrollOptions] - scrolling options.
58
+ * @param {object} [asyncLoader] - loader to use while processing initial async resources
31
59
  * @returns {object} a component.
32
60
  */
33
- function Router(routes) {
61
+ function Router(routes, scrollOptions = {hashed: {}, unhashed: {to: {x: 0, y: 0}}}, asyncLoader = {}) {
34
62
  return {
35
- render() {
63
+ state() {
36
64
  const page = getPage();
37
65
  const route = getRoute(routes, page);
38
- let element = (() => {
39
- if (Object.getPrototypeOf(route.route).constructor.name === "AsyncFunction") {
40
- route.route(page).then(v => {
41
- replaceWith(this.element.firstChild, v);
42
- });
43
- return {};
44
- } else {
45
- return route.route(page);
46
- }
47
- })();
48
66
  return {
49
- children: typeof element === 'object' ? {...element, key: page} : element,
67
+ element: (() => {
68
+ if (Object.getPrototypeOf(route.route).constructor.name === "AsyncFunction") {
69
+ route.route(page).then(v => {
70
+ document.getElementById('router').dispatchEvent(new CustomEvent('initrouterload', {detail: {element: v}}));
71
+ });
72
+ return asyncLoader;
73
+ } else {
74
+ return route.route(page);
75
+ }
76
+ })()
77
+ }
78
+ },
79
+ render() {
80
+ return {
81
+ children: typeof this.state.element === 'object' ?
82
+ {...this.state.element, key: `route-${getPage()}`} : this.state.element,
50
83
  id: 'router',
51
84
  dataset: { receivePageChanges: true },
52
85
  on: {
@@ -55,18 +88,28 @@ function Router(routes) {
55
88
  const route = getRoute(routes, page);
56
89
  if (Object.getPrototypeOf(route.route).constructor.name === "AsyncFunction") {
57
90
  route.route(page).then(v => {
58
- this.element.scrollTo(0, 0);
59
- replaceWith(this.element.firstChild, v);
91
+ this.state.element = v;
92
+ this.rerender();
93
+ handleScrolling(this.element, scrollOptions);
60
94
  });
61
95
  } else {
62
- this.element.scrollTo(0, 0);
96
+ this.state.element = route.route(page);
63
97
  this.rerender();
98
+ handleScrolling(this.element, scrollOptions);
64
99
  }
65
100
  document.title = getRoute(routes, getPage()).title ?? document.title;
66
101
  },
67
102
  mount() {
68
103
  window.onpopstate = () => broadcastPageChange(getPage());
69
- document.title = route.title ?? document.title;
104
+ const page = getPage();
105
+ const route = getRoute(routes, page);
106
+ if (Object.getPrototypeOf(route.route).constructor.name !== "AsyncFunction")
107
+ handleScrolling(this.element, scrollOptions);
108
+ },
109
+ initrouterload(e) {
110
+ this.state.element = e.detail.element;
111
+ this.rerender();
112
+ handleScrolling(this.element, scrollOptions);
70
113
  }
71
114
  }
72
115
  }
@@ -81,9 +124,15 @@ function Router(routes) {
81
124
  */
82
125
  function navigate(page) {
83
126
  if (page.startsWith('/')) page = page.slice(1);
127
+ let hash = '';
128
+ if (page.includes('#')) {
129
+ hash = page.slice(page.indexOf('#'));
130
+ page = page.slice(0, page.indexOf('#'));
131
+ }
84
132
  if (page === getPage()) return;
85
133
  const url = new URL(window.location.href);
86
134
  url.searchParams.set(PARAM_NAME, page);
135
+ url.hash = hash;
87
136
  window.history.pushState({}, '', url);
88
137
  broadcastPageChange(page);
89
138
  }
@@ -99,4 +148,4 @@ function broadcastPageChange(page) {
99
148
  });
100
149
  }
101
150
 
102
- export { navigate, Router, getPage }
151
+ export { navigate, Router, getPage };