diff --git a/src/data/action.ts b/src/data/action.ts index 35b1ce246..cf9381384 100644 --- a/src/data/action.ts +++ b/src/data/action.ts @@ -18,6 +18,8 @@ export type Action, U> = (T extends [FormData] | [] export const actions = /* #__PURE__ */ new Map>(); +export const routableForms = /* #__PURE__ */ new Set(); + export function useSubmissions, U>( fn: Action, filter?: (arg: T) => boolean diff --git a/src/data/components.tsx b/src/data/components.tsx new file mode 100644 index 000000000..5366db893 --- /dev/null +++ b/src/data/components.tsx @@ -0,0 +1,18 @@ +/*@refresh skip*/ +import type { JSX } from "solid-js"; +import { onCleanup, onMount, splitProps } from "solid-js"; +import { routableForms } from "./action.js"; + +export interface FormProps extends JSX.FormHTMLAttributes {} +export function Form(props: FormProps) { + const [, rest] = splitProps(props, ["ref"]); + onMount(() => { + routableForms.add(formRef) + }) + onCleanup(() => routableForms.delete(formRef)) + let formRef: HTMLFormElement; + return
{ + props.ref && (props.ref as Function)(el); + formRef = el + }} /> +} \ No newline at end of file diff --git a/src/data/events.ts b/src/data/events.ts index b10d90b36..c396554d7 100644 --- a/src/data/events.ts +++ b/src/data/events.ts @@ -1,7 +1,7 @@ import { delegateEvents } from "solid-js/web"; import { onCleanup } from "solid-js"; import type { RouterContext } from "../types.js"; -import { actions } from "./action.js"; +import { actions, routableForms } from "./action.js"; import { mockBase } from "../utils.js"; export function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") { @@ -103,13 +103,33 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB ? evt.submitter.getAttribute("formaction") : (evt.target as HTMLElement).getAttribute("action"); if (!actionRef) return; + const method = + evt.submitter && evt.submitter.hasAttribute("formmethod") + ? evt.submitter.getAttribute("formmethod") + : (evt.target as HTMLElement).getAttribute("method"); + if (method?.toUpperCase() === "GET") { + if (routableForms.has(evt.target as HTMLFormElement)) { + evt.preventDefault(); + const data = new FormData(evt.target as HTMLFormElement); + if (evt.submitter && (evt.submitter as HTMLButtonElement | HTMLInputElement).name) + data.append( + (evt.submitter as HTMLButtonElement | HTMLInputElement).name, + (evt.submitter as HTMLButtonElement | HTMLInputElement).value + ); + const url = new URL(actionRef, location.origin); + url.search = "?" + [...data.entries()].map(([key, value]) => `${key}=${value}`).join("&"); + const to = router.parsePath(url.pathname + url.search + url.hash); + navigateFromRoute(to, { resolve: false }); + return; + } + } if (!actionRef.startsWith("https://action/")) { // normalize server actions const url = new URL(actionRef, mockBase); actionRef = router.parsePath(url.pathname + url.search); if (!actionRef.startsWith(actionBase)) return; } - if ((evt.target as HTMLFormElement).method.toUpperCase() !== "POST") + if (method?.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions"); const handler = actions.get(actionRef); if (handler) { diff --git a/src/data/index.ts b/src/data/index.ts index 9c3809869..ce9d17bcd 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -2,4 +2,4 @@ export { createAsync, createAsyncStore } from "./createAsync.js"; export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js"; export { cache, revalidate, type CachedFunction } from "./cache.js"; export { redirect, reload, json } from "./response.js"; - +export * from "./components.jsx";