router-dom
Advanced tools
Comparing version 1.2.0 to 1.2.1
@@ -6,3 +6,3 @@ export default class Router { | ||
constructor(routes: [RouteParam, ...RouteParam[]], options?: Options); | ||
private triggerEvent; | ||
private doRouting; | ||
go(path: string, state: LooseObject, params?: string): void; | ||
@@ -37,3 +37,4 @@ removeRoute(path: string): void; | ||
interface Options { | ||
errorHandler?(e: Error): Promise<any> | void; | ||
errorHandler?(err: Error, e?: PopStateEvent | Event): Promise<any> | void; | ||
formHandler?(res: Response, e: Event): Promise<any> | void; | ||
} | ||
@@ -40,0 +41,0 @@ interface RoutingProps { |
import { listen } from "quicklink"; | ||
import { pathToRegexp } from "path-to-regexp"; | ||
import { render, html, hydro, $ } from "hydro-js"; | ||
import { render, html, hydro, $, $$ } from "hydro-js"; | ||
listen(); | ||
@@ -13,58 +13,5 @@ let router; | ||
} | ||
addEventListener("popstate", async () => { | ||
const to = location.pathname; | ||
const from = router.oldRoute ?? to; | ||
const route = getMatchingRoute(to); | ||
if (route) { | ||
try { | ||
const [_, ...values] = to.match(route.path); | ||
const params = Array.from(route.originalPath.matchAll(flagsRegex)) | ||
.flat() | ||
.map((i) => i.replace(":", "")) | ||
.reduce((state, key, idx) => { | ||
state[key] = values[idx]; | ||
return state; | ||
}, {}); | ||
const allParams = { ...router.getParams(), ...params }; | ||
const props = { | ||
from: from.replace(base, ""), | ||
to: to.replace(base, ""), | ||
...(Object.keys(allParams).length ? { params: allParams } : {}), | ||
...history.state, | ||
}; | ||
// Trigger leave | ||
if (router.oldRoute) { | ||
const oldRoute = router.routes.find((route) => route.path.exec(router.oldRoute)); | ||
if (oldRoute) { | ||
await oldRoute["leave" /* leave */]?.(props); | ||
router.oldRoute = route.originalPath; | ||
} | ||
} | ||
// Trigger beforeEnter | ||
await route["beforeEnter" /* beforeEnter */]?.(props); | ||
// Handle template / element | ||
if (route?.templateUrl) { | ||
const data = await fetch(route.templateUrl); | ||
const _html = await data.text(); | ||
render(html `<div data-outlet>${_html}</div>`, outletSelector, false); | ||
} | ||
else if (route?.element) { | ||
render(html `<div data-outlet>${route?.element}</div>`, outletSelector, false); | ||
} | ||
else { | ||
// Clear outlet | ||
$(outletSelector).textContent = null; | ||
} | ||
// Trigger afterEnter | ||
await route["afterEnter" /* afterEnter */]?.(props); | ||
} | ||
catch (e) { | ||
if (router.options.errorHandler) { | ||
await router.options.errorHandler(e); | ||
} | ||
else { | ||
console.error(e); | ||
} | ||
} | ||
} | ||
addEventListener("popstate", async (e) => { | ||
//@ts-ignore | ||
router.doRouting(location.pathname, e); | ||
}); | ||
@@ -83,13 +30,71 @@ export default class Router { | ||
router = this; | ||
this.triggerEvent(); | ||
this.doRouting(); | ||
} | ||
triggerEvent() { | ||
dispatchEvent(new Event("popstate")); | ||
async doRouting(to = location.pathname, e) { | ||
dispatchEvent(new Event("beforeRouting")); | ||
const from = this.oldRoute ?? to; | ||
const route = getMatchingRoute(to); | ||
if (route) { | ||
try { | ||
const [_, ...values] = to.match(route.path); | ||
const params = Array.from(route.originalPath.matchAll(flagsRegex)) | ||
.flat() | ||
.map((i) => i.replace(":", "")) | ||
.reduce((state, key, idx) => { | ||
state[key] = values[idx]; | ||
return state; | ||
}, {}); | ||
const allParams = { ...this.getParams(), ...params }; | ||
const props = { | ||
from: from.replace(base, ""), | ||
to: to.replace(base, ""), | ||
...(Object.keys(allParams).length ? { params: allParams } : {}), | ||
...history.state, | ||
}; | ||
// Trigger leave | ||
if (this.oldRoute) { | ||
const oldRoute = this.routes.find((route) => route.path.exec(this.oldRoute)); | ||
if (oldRoute) { | ||
await oldRoute["leave" /* leave */]?.(props); | ||
this.oldRoute = route.originalPath; | ||
} | ||
} | ||
// Trigger beforeEnter | ||
await route["beforeEnter" /* beforeEnter */]?.(props); | ||
// Handle template / element | ||
if (route?.templateUrl) { | ||
const data = await fetch(route.templateUrl); | ||
const _html = await data.text(); | ||
render(html `<div data-outlet>${_html}</div>`, outletSelector, false); | ||
} | ||
else if (route?.element) { | ||
render(html `<div data-outlet>${route?.element}</div>`, outletSelector, false); | ||
} | ||
else { | ||
// Clear outlet | ||
$(outletSelector).textContent = null; | ||
} | ||
// Trigger afterEnter | ||
await route["afterEnter" /* afterEnter */]?.(props); | ||
} | ||
catch (err) { | ||
if (this.options.errorHandler) { | ||
await this.options.errorHandler(err, e); | ||
} | ||
else { | ||
console.error(err, e); | ||
} | ||
} | ||
finally { | ||
dispatchEvent(new Event("afterRouting")); | ||
} | ||
} | ||
} | ||
go(path, state, params = "") { | ||
this.oldRoute = location.pathname; | ||
const newPath = base + path + params; | ||
// Only navigate when the path differs | ||
if (path !== this.oldRoute) { | ||
history.pushState({ ...state }, "", base + path + params); | ||
this.triggerEvent(); | ||
if (newPath !== this.oldRoute) { | ||
history.pushState({ ...state }, "", newPath); | ||
this.doRouting(newPath); | ||
} | ||
@@ -144,2 +149,26 @@ } | ||
} | ||
function registerFormEvent(form) { | ||
form.addEventListener("submit", (e) => { | ||
if (!router.options.formHandler) | ||
return; | ||
e.preventDefault(); | ||
const action = form.getAttribute("action"); | ||
const method = form.getAttribute("method"); | ||
fetch(action, { | ||
method, | ||
...(!["HEAD", "GET"].includes(method.toUpperCase()) | ||
? { body: new FormData(form) } | ||
: {}), | ||
}) | ||
.then((res) => router.options.formHandler(res, e)) | ||
.catch(async (err) => { | ||
if (router.options.errorHandler) { | ||
await router.options.errorHandler(err, e); | ||
} | ||
else { | ||
console.error(err, e); | ||
} | ||
}); | ||
}); | ||
} | ||
function replaceBars(hydroTerm) { | ||
@@ -151,10 +180,11 @@ if (hydroTerm === null || !hydroTerm.includes("{{")) | ||
} | ||
// Add EventListener for every added anchor Element | ||
document.body.querySelectorAll("a").forEach(registerAnchorEvent); | ||
// Add EventListener for every added anchor and form Element | ||
$$("a").forEach(registerAnchorEvent); | ||
$$("form").forEach(registerFormEvent); | ||
new MutationObserver((entries) => { | ||
for (const entry of entries) { | ||
for (const node of entry.addedNodes) { | ||
const anchors = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT, { | ||
const nodes = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT, { | ||
acceptNode(elem) { | ||
return elem.localName === "a" | ||
return ["form", "a"].includes(elem.localName) | ||
? NodeFilter.FILTER_ACCEPT | ||
@@ -164,5 +194,10 @@ : NodeFilter.FILTER_REJECT; | ||
}); | ||
let anchor; | ||
while ((anchor = anchors.nextNode())) { | ||
registerAnchorEvent(anchor); | ||
let formOrA; | ||
while ((formOrA = nodes.nextNode())) { | ||
if (formOrA.localName === "a") { | ||
registerAnchorEvent(formOrA); | ||
} | ||
else { | ||
registerFormEvent(formOrA); | ||
} | ||
} | ||
@@ -169,0 +204,0 @@ } |
{ | ||
"name": "router-dom", | ||
"version": "1.2.0", | ||
"version": "1.2.1", | ||
"description": "A lightweight router for everyone", | ||
@@ -40,3 +40,3 @@ "type": "module", | ||
"dependencies": { | ||
"hydro-js": "^1.4.0", | ||
"hydro-js": "^1.4.1", | ||
"path-to-regexp": "^6.2.0", | ||
@@ -43,0 +43,0 @@ "quicklink": "^2.1.0" |
@@ -9,2 +9,3 @@ # router-dom | ||
> - base href support. | ||
> - opt-in errorHandler and formHandler. | ||
> - support in all modern browsers. | ||
@@ -51,5 +52,9 @@ | ||
### Events | ||
- window: beforeRouting & afterRouting | ||
### Constructor | ||
The router class takes an array with at least one entry. Only the path is mandatory. Either a template or and element will be rendered in your element with attribute `data-outlet`. The second argument is the optional object options: it can take a general errorHandler. | ||
The router class takes an array with at least one entry. Only the path is mandatory. Either a template or and element will be rendered in your element with attribute `data-outlet`. The second argument is the optional object options: it can take a general errorHandler and a formHandler. If there is a formHandler, form submits will handled via attributes on the form element and fetch. | ||
@@ -101,3 +106,2 @@ ```js | ||
- Handle Form Submits | ||
- Add nested routes |
import { listen } from "quicklink"; | ||
import { pathToRegexp } from "path-to-regexp"; | ||
import { render, html, hydro, $ } from "hydro-js"; | ||
import { render, html, hydro, $, $$ } from "hydro-js"; | ||
@@ -16,66 +16,5 @@ listen(); | ||
addEventListener("popstate", async () => { | ||
const to = location.pathname; | ||
const from = router.oldRoute ?? to; | ||
const route = getMatchingRoute(to); | ||
if (route) { | ||
try { | ||
const [_, ...values] = to.match(route.path)!; | ||
const params = Array.from(route.originalPath.matchAll(flagsRegex)) | ||
.flat() | ||
.map((i) => i.replace(":", "")) | ||
.reduce((state: LooseObject, key, idx) => { | ||
state[key] = values[idx]; | ||
return state; | ||
}, {}); | ||
const allParams = { ...router.getParams(), ...params }; | ||
const props = { | ||
from: from.replace(base, ""), | ||
to: to.replace(base, ""), | ||
...(Object.keys(allParams).length ? { params: allParams } : {}), | ||
...history.state, | ||
}; | ||
// Trigger leave | ||
if (router.oldRoute) { | ||
const oldRoute = router.routes.find((route) => | ||
route.path.exec(router.oldRoute!) | ||
); | ||
if (oldRoute) { | ||
await oldRoute[cycles.leave]?.(props); | ||
router.oldRoute = route.originalPath; | ||
} | ||
} | ||
// Trigger beforeEnter | ||
await route[cycles.beforeEnter]?.(props); | ||
// Handle template / element | ||
if (route?.templateUrl) { | ||
const data = await fetch(route.templateUrl); | ||
const _html = await data.text(); | ||
render(html`<div data-outlet>${_html}</div>`, outletSelector, false); | ||
} else if (route?.element) { | ||
render( | ||
html`<div data-outlet>${route?.element}</div>`, | ||
outletSelector, | ||
false | ||
); | ||
} else { | ||
// Clear outlet | ||
$(outletSelector)!.textContent = null; | ||
} | ||
// Trigger afterEnter | ||
await route[cycles.afterEnter]?.(props); | ||
} catch (e) { | ||
if (router.options.errorHandler) { | ||
await router.options.errorHandler(e); | ||
} else { | ||
console.error(e); | ||
} | ||
} | ||
} | ||
addEventListener("popstate", async (e) => { | ||
//@ts-ignore | ||
router.doRouting(location.pathname, e); | ||
}); | ||
@@ -101,7 +40,70 @@ | ||
this.triggerEvent(); | ||
this.doRouting(); | ||
} | ||
private triggerEvent() { | ||
dispatchEvent(new Event("popstate")); | ||
private async doRouting(to: string = location.pathname, e?: PopStateEvent) { | ||
dispatchEvent(new Event("beforeRouting")); | ||
const from = this.oldRoute ?? to; | ||
const route = getMatchingRoute(to); | ||
if (route) { | ||
try { | ||
const [_, ...values] = to.match(route.path)!; | ||
const params = Array.from(route.originalPath.matchAll(flagsRegex)) | ||
.flat() | ||
.map((i) => i.replace(":", "")) | ||
.reduce((state: LooseObject, key, idx) => { | ||
state[key] = values[idx]; | ||
return state; | ||
}, {}); | ||
const allParams = { ...this.getParams(), ...params }; | ||
const props = { | ||
from: from.replace(base, ""), | ||
to: to.replace(base, ""), | ||
...(Object.keys(allParams).length ? { params: allParams } : {}), | ||
...history.state, | ||
}; | ||
// Trigger leave | ||
if (this.oldRoute) { | ||
const oldRoute = this.routes.find((route) => | ||
route.path.exec(this.oldRoute!) | ||
); | ||
if (oldRoute) { | ||
await oldRoute[cycles.leave]?.(props); | ||
this.oldRoute = route.originalPath; | ||
} | ||
} | ||
// Trigger beforeEnter | ||
await route[cycles.beforeEnter]?.(props); | ||
// Handle template / element | ||
if (route?.templateUrl) { | ||
const data = await fetch(route.templateUrl); | ||
const _html = await data.text(); | ||
render(html`<div data-outlet>${_html}</div>`, outletSelector, false); | ||
} else if (route?.element) { | ||
render( | ||
html`<div data-outlet>${route?.element}</div>`, | ||
outletSelector, | ||
false | ||
); | ||
} else { | ||
// Clear outlet | ||
$(outletSelector)!.textContent = null; | ||
} | ||
// Trigger afterEnter | ||
await route[cycles.afterEnter]?.(props); | ||
} catch (err) { | ||
if (this.options.errorHandler) { | ||
await this.options.errorHandler(err, e); | ||
} else { | ||
console.error(err, e); | ||
} | ||
} finally { | ||
dispatchEvent(new Event("afterRouting")); | ||
} | ||
} | ||
} | ||
@@ -111,8 +113,8 @@ | ||
this.oldRoute = location.pathname; | ||
const newPath = base + path + params; | ||
// Only navigate when the path differs | ||
if (path !== this.oldRoute) { | ||
history.pushState({ ...state }, "", base + path + params); | ||
this.triggerEvent(); | ||
if (newPath !== this.oldRoute) { | ||
history.pushState({ ...state }, "", newPath); | ||
this.doRouting(newPath); | ||
} | ||
@@ -179,2 +181,27 @@ } | ||
function registerFormEvent(form: HTMLFormElement) { | ||
form.addEventListener("submit", (e) => { | ||
if (!router.options.formHandler) return; | ||
e.preventDefault(); | ||
const action = form.getAttribute("action")!; | ||
const method = form.getAttribute("method")!; | ||
fetch(action, { | ||
method, | ||
...(!["HEAD", "GET"].includes(method.toUpperCase()) | ||
? { body: new FormData(form) } | ||
: {}), | ||
}) | ||
.then((res) => router.options.formHandler!(res, e)) | ||
.catch(async (err) => { | ||
if (router.options.errorHandler) { | ||
await router.options.errorHandler(err, e); | ||
} else { | ||
console.error(err, e); | ||
} | ||
}); | ||
}); | ||
} | ||
function replaceBars(hydroTerm: string | null) { | ||
@@ -187,21 +214,24 @@ if (hydroTerm === null || !hydroTerm.includes("{{")) return hydroTerm; | ||
// Add EventListener for every added anchor Element | ||
document.body.querySelectorAll("a").forEach(registerAnchorEvent); | ||
// Add EventListener for every added anchor and form Element | ||
$$("a").forEach(registerAnchorEvent); | ||
$$("form").forEach(registerFormEvent); | ||
new MutationObserver((entries) => { | ||
for (const entry of entries) { | ||
for (const node of entry.addedNodes) { | ||
const anchors = document.createNodeIterator( | ||
node, | ||
NodeFilter.SHOW_ELEMENT, | ||
{ | ||
acceptNode(elem: Element) { | ||
return elem.localName === "a" | ||
? NodeFilter.FILTER_ACCEPT | ||
: NodeFilter.FILTER_REJECT; | ||
}, | ||
const nodes = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT, { | ||
acceptNode(elem: Element) { | ||
return ["form", "a"].includes(elem.localName) | ||
? NodeFilter.FILTER_ACCEPT | ||
: NodeFilter.FILTER_REJECT; | ||
}, | ||
}); | ||
let formOrA: HTMLAnchorElement | HTMLFormElement; | ||
while ( | ||
(formOrA = nodes.nextNode() as HTMLAnchorElement | HTMLFormElement) | ||
) { | ||
if (formOrA.localName === "a") { | ||
registerAnchorEvent(formOrA as HTMLAnchorElement); | ||
} else { | ||
registerFormEvent(formOrA as HTMLFormElement); | ||
} | ||
); | ||
let anchor; | ||
while ((anchor = anchors.nextNode() as HTMLAnchorElement)) { | ||
registerAnchorEvent(anchor); | ||
} | ||
@@ -232,3 +262,4 @@ } | ||
interface Options { | ||
errorHandler?(e: Error): Promise<any> | void; | ||
errorHandler?(err: Error, e?: PopStateEvent | Event): Promise<any> | void; | ||
formHandler?(res: Response, e: Event): Promise<any> | void; | ||
} | ||
@@ -235,0 +266,0 @@ interface RoutingProps { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
46080
12
499
105
2
Updatedhydro-js@^1.4.1