feat(runtime): add URL-state GET navigation

This commit is contained in:
slhx agent
2026-06-29 18:21:00 +02:00
parent 9dd8e4a944
commit d0e7022c86
10 changed files with 185 additions and 97 deletions
+39 -4
View File
@@ -190,6 +190,36 @@
return url.href;
}
function pageRequestUrl(form, source) {
const url = new URL((form && form.action) || location.href, location.href);
url.search = urlEncoded(successfulFormData(form, source)).toString();
return url.href;
}
function successfulFormData(form, source) {
try {
return source && source !== form ? new FormData(form, source) : new FormData(form);
} catch (_) {
return new FormData(form);
}
}
function pageFormHistoryMode(source, form) {
return historyMode(source, null) || historyMode(form, null) || historyMode(boostRoot(form), null);
}
function boostRoot(el) {
return el && closestInRoot(el.parentElement, rootOf(el), (node) => node.hasAttribute("data-hemx-boost"));
}
function historyMode(el, fallback) {
if (!el) return fallback;
const value = el.getAttribute("data-hemx-history") || el.getAttribute("data-hemx-nav") || "";
if (value === "push" || value === "replace" || value === "none") return value;
if (el.hasAttribute("data-hemx-history") || el.hasAttribute("data-hemx-nav")) return fallback || "push";
return fallback;
}
function httpError(response) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
@@ -233,6 +263,7 @@
let finish;
const done = new Promise((resolve) => { finish = resolve; });
const method = String((form && form.getAttribute("method")) || "POST").toUpperCase();
const mode = method === "GET" && form && pageFormHistoryMode(source, form);
const body = method === "GET" || method === "HEAD" ? undefined : requestBody(data, multipart);
const headers = { "X-HEMX-Partial": "1", "Accept": "application/hemx, text/html" };
if (body instanceof URLSearchParams) headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
@@ -240,6 +271,10 @@
showError(target, null);
showPending(target, true);
try {
if (mode) {
await navigateUrl(pageRequestUrl(form, source), rootOf(form) || rootOf(el), mode);
return;
}
const response = await fetch(requestUrl(form, data, method), {
method,
body,
@@ -629,7 +664,7 @@
);
if (name === "click" && nav && sameOriginNav(event, nav)) {
event.preventDefault();
navigate(nav);
navigate(nav, historyMode(nav, "push"));
return;
}
if (name === "click") {
@@ -646,7 +681,7 @@
(el.tagName === "INPUT" && el.getAttribute("type") === "submit")
);
const form = formOwner(submitter);
if (form && root.contains(form) && formHandleId(form)) {
if (form && root.contains(form) && (formHandleId(form) || pageFormHistoryMode(submitter, form))) {
if (form.reportValidity && !form.reportValidity()) return;
event.preventDefault();
schedule(form, "submit", submitter);
@@ -655,9 +690,9 @@
}
let el = closestInRoot(event.target, root, (node) =>
node.hasAttribute(HID) ||
(node.tagName === "FORM" && closestInRoot(node.parentElement, root, (parent) => parent.hasAttribute("data-hemx-boost")))
(node.tagName === "FORM" && (pageFormHistoryMode(event.submitter || node, node) || boostRoot(node)))
);
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && formHandleId(event.target)) el = event.target;
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && (formHandleId(event.target) || pageFormHistoryMode(event.submitter || event.target, event.target))) el = event.target;
if (!el || defaultEvent(el) !== name) return;
event.preventDefault();
schedule(el, name, event.submitter || el);
+15
View File
@@ -43,6 +43,21 @@ fn runtime_posts_urlencoded_forms_by_default() {
assert!(source.contains("application/x-www-form-urlencoded;charset=UTF-8"));
}
#[test]
fn runtime_turns_page_get_forms_into_url_state_navigation() {
// req: page_swap/009 req: page_swap/010
let source = hemx_js::RUNTIME_JS;
assert!(source.contains("function pageFormHistoryMode(source, form)"));
assert!(source.contains("const mode = method === \"GET\" && form && pageFormHistoryMode(source, form)"));
assert!(source.contains("if (mode)"));
assert!(source.contains("await navigateUrl(pageRequestUrl(form, source), rootOf(form) || rootOf(el), mode)"));
assert!(source.contains("function successfulFormData(form, source)"));
assert!(source.contains("new FormData(form, source)"));
assert!(source.contains("data-hemx-nav"));
assert!(source.contains("data-hemx-history"));
}
#[test]
fn runtime_preserves_multipart_file_upload_fallback_shape() {
// req: multipart/003