Skip to content

Commit ca34b9e

Browse files
committed
feat: allow gui.set_session_state from client js
refactor: streamline form submission handling and enhance form data transformation logic
1 parent d5081a0 commit ca34b9e

File tree

2 files changed

+93
-113
lines changed

2 files changed

+93
-113
lines changed

app/app.tsx

Lines changed: 62 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import { withSentry } from "@sentry/remix";
2-
import { useEffect, useRef } from "react";
2+
import { FormEvent, useEffect, useRef } from "react";
33

4-
import {
5-
ActionArgs,
6-
json,
7-
LinksFunction,
8-
LoaderArgs,
9-
redirect,
10-
} from "@remix-run/node";
4+
import { json, LinksFunction, redirect } from "@remix-run/node";
115
import type {
126
ShouldRevalidateFunction,
7+
SubmitOptions,
138
V2_MetaFunction,
149
} from "@remix-run/react";
1510
import {
@@ -23,9 +18,9 @@ import {
2318
} from "@remix-run/react";
2419
import path from "path";
2520
import { useDebouncedCallback } from "use-debounce";
26-
import { applyTransform, getTransforms, RenderedChildren } from "~/renderer";
2721
import { useEventSourceNullOk } from "~/event-source";
2822
import { handleRedirectResponse } from "~/handleRedirect";
23+
import { applyFormDataTransforms, RenderedChildren } from "~/renderer";
2924

3025
import { gooeyGuiRouteHeader } from "~/consts";
3126
import appStyles from "~/styles/app.css";
@@ -64,53 +59,7 @@ export const links: LinksFunction = () => {
6459
];
6560
};
6661

67-
export async function loader({ request }: LoaderArgs) {
68-
return await callServer({ request });
69-
}
70-
71-
export async function action({ request }: ActionArgs) {
72-
// proxy
73-
let contentType = request.headers.get("Content-Type");
74-
if (!contentType?.startsWith("application/x-www-form-urlencoded")) {
75-
let body = await request.arrayBuffer();
76-
return callServer({ request, body });
77-
}
78-
// proxy
79-
let body = await request.text();
80-
let formData = new URLSearchParams(body);
81-
if (!formData.has("__gooey_gui_request_body")) {
82-
return callServer({ request, body });
83-
}
84-
85-
// parse request body
86-
let { __gooey_gui_request_body, ...inputs } = Object.fromEntries(formData);
87-
const {
88-
transforms,
89-
state,
90-
...jsonBody
91-
}: {
92-
transforms: Record<string, string>;
93-
state: Record<string, any>;
94-
} & Record<string, any> = JSON.parse(__gooey_gui_request_body.toString());
95-
// apply transforms
96-
for (let [field, inputType] of Object.entries(transforms)) {
97-
let toJson = applyTransform[inputType];
98-
if (!toJson) continue;
99-
inputs[field] = toJson(inputs[field]);
100-
}
101-
// update state with new form data
102-
jsonBody.state = { ...state, ...inputs };
103-
request.headers.set("Content-Type", "application/json");
104-
return callServer({ request, body: JSON.stringify(jsonBody) });
105-
}
106-
107-
async function callServer({
108-
request,
109-
body,
110-
}: {
111-
request: Request;
112-
body?: BodyInit | null;
113-
}) {
62+
export async function loader({ request }: { request: Request }) {
11463
const requestUrl = new URL(request.url);
11564
const serverUrl = new URL(settings.SERVER_HOST!);
11665
serverUrl.pathname = path.join(serverUrl.pathname, requestUrl.pathname ?? "");
@@ -119,10 +68,15 @@ async function callServer({
11968
request.headers.delete("Host");
12069
request.headers.set(gooeyGuiRouteHeader, "1");
12170

71+
let body;
72+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method)) {
73+
body = await request.arrayBuffer();
74+
}
75+
12276
let response = await fetch(serverUrl, {
12377
method: request.method,
12478
redirect: "manual",
125-
body: body,
79+
body,
12680
headers: request.headers,
12781
});
12882

@@ -147,6 +101,8 @@ async function callServer({
147101
}
148102
}
149103

104+
export const action = loader;
105+
150106
export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
151107
if (
152108
// don't revalidate if its a form submit with successful response
@@ -200,21 +156,6 @@ function App() {
200156
const submit = useSubmit();
201157
const navigate = useNavigate();
202158

203-
if (typeof window !== "undefined") {
204-
// @ts-ignore
205-
window.gui = {
206-
session_state: state,
207-
navigate,
208-
fetcher,
209-
submit() {
210-
if (formRef.current) submit(formRef.current, ...arguments);
211-
},
212-
rerun() {
213-
if (formRef.current) submit(formRef.current);
214-
},
215-
};
216-
}
217-
218159
useEffect(() => {
219160
if (!base64Body) return;
220161
let body = base64Decode(base64Body);
@@ -225,15 +166,10 @@ function App() {
225166

226167
useEffect(() => {
227168
if (realtimeEvent && fetcher.state === "idle" && formRef.current) {
228-
submit(formRef.current);
169+
onSubmit();
229170
}
230171
}, [fetcher.state, realtimeEvent, submit]);
231172

232-
const debouncedSubmit = useDebouncedCallback((form: HTMLFormElement) => {
233-
form.removeAttribute("debounceInProgress");
234-
submit(form);
235-
}, 500);
236-
237173
const onChange: OnChange = (event) => {
238174
const target = event?.target;
239175
const form = event?.currentTarget || formRef?.current;
@@ -267,40 +203,75 @@ function App() {
267203
"focusout",
268204
function () {
269205
form.removeAttribute("debounceInProgress");
270-
submit(form);
206+
onSubmit();
271207
},
272208
{ once: true }
273209
);
274210
} else {
275-
submit(form);
211+
onSubmit();
276212
}
277213
};
278214

279-
if (!children) return <></>;
215+
const debouncedSubmit = useDebouncedCallback((form: HTMLFormElement) => {
216+
form.removeAttribute("debounceInProgress");
217+
onSubmit();
218+
}, 500);
280219

281-
const transforms = getTransforms({ children });
220+
let submitOptions: SubmitOptions = {
221+
method: "post",
222+
action: "?" + searchParams,
223+
encType: "application/json",
224+
};
225+
226+
const onSubmit = (event?: FormEvent) => {
227+
if (!formRef.current) return;
228+
let formData = Object.fromEntries(new FormData(formRef.current));
229+
if (event) {
230+
event.preventDefault();
231+
let submitter = (event.nativeEvent as SubmitEvent)
232+
.submitter as HTMLFormElement;
233+
if (submitter) {
234+
formData[submitter.name] = submitter.value;
235+
}
236+
}
237+
applyFormDataTransforms({ children, formData });
238+
let body = { state: { ...state, ...formData } };
239+
submit(body, submitOptions);
240+
};
241+
242+
if (typeof window !== "undefined") {
243+
// @ts-ignore
244+
window.gui = {
245+
navigate,
246+
fetcher,
247+
session_state: state,
248+
update_session_state(newState: Record<string, any>) {
249+
submit({ state: { ...state, ...newState } }, submitOptions);
250+
},
251+
set_session_state(newState: Record<string, any>) {
252+
submit({ state: newState }, submitOptions);
253+
},
254+
rerun: onSubmit,
255+
};
256+
}
257+
258+
if (!children) return <></>;
282259

283260
return (
284261
<div data-prismjs-copy="📋 Copy" data-prismjs-copy-success="✅ Copied!">
285-
<Form
262+
<form
286263
ref={formRef}
287264
id={"gooey-form"}
288-
action={"?" + searchParams}
289-
method="POST"
290265
onChange={onChange}
266+
onSubmit={onSubmit}
291267
noValidate
292268
>
293269
<RenderedChildren
294270
children={children}
295271
onChange={onChange}
296272
state={state}
297273
/>
298-
<input
299-
type="hidden"
300-
name="__gooey_gui_request_body"
301-
value={JSON.stringify({ state, transforms })}
302-
/>
303-
</Form>
274+
</form>
304275
<script
305276
async
306277
defer

app/renderer.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,37 +76,46 @@ export type TreeNode = {
7676
children: Array<TreeNode>;
7777
};
7878

79-
export function getTransforms({
79+
export function applyFormDataTransforms({
8080
children,
81+
formData,
8182
}: {
8283
children: Array<TreeNode>;
83-
}): Record<string, string> {
84-
let ret: Record<string, string> = {};
84+
formData: Record<string, FormDataEntryValue | null>;
85+
}) {
8586
for (const node of children) {
8687
const { name, props, children } = node;
87-
switch (name) {
88-
case "input":
89-
ret[props.name] = props.type;
90-
break;
91-
default:
92-
ret[props.name] = name;
93-
break;
88+
if (children) {
89+
applyFormDataTransforms({ children, formData });
90+
}
91+
let type;
92+
if (name === "input") {
93+
type = props.type;
94+
} else {
95+
type = name;
9496
}
95-
if (!children) continue;
96-
ret = { ...ret, ...getTransforms({ children }) };
97+
let transform = formDataTransforms[type];
98+
if (!transform) continue;
99+
formData[props.name] = transform(formData[props.name]);
97100
}
98-
return ret;
99101
}
100102

101-
export const applyTransform: Record<string, (val: FormDataEntryValue) => any> =
102-
{
103-
checkbox: Boolean,
104-
number: parseIntFloat,
105-
range: parseIntFloat,
106-
select: (val) => (val ? JSON.parse(`${val}`) : null),
107-
file: (val) => (val ? JSON.parse(`${val}`) : null),
108-
switch: Boolean,
109-
};
103+
const formDataTransforms: Record<
104+
string,
105+
(val: FormDataEntryValue | null) => any
106+
> = {
107+
checkbox: Boolean,
108+
switch: Boolean,
109+
number: parseIntFloat,
110+
range: parseIntFloat,
111+
select: parseJSON,
112+
file: parseJSON,
113+
};
114+
115+
function parseJSON(val: FormDataEntryValue | null) {
116+
if (!val) return null;
117+
return JSON.parse(val.toString());
118+
}
110119

111120
function parseIntFloat(
112121
val: FormDataEntryValue | undefined | null

0 commit comments

Comments
 (0)