Skip to content

Commit e19e5f8

Browse files
committed
WIP form helper
1 parent ad298e6 commit e19e5f8

3 files changed

Lines changed: 119 additions & 5 deletions

File tree

src/dom.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { axios, type RouterRequestConfig, type VortexConfig } from "./router"
1+
import { axios, RouterResponse, type RouterRequestConfig, type VortexConfig } from "./router"
2+
import { useForm, VortexForm } from "./form"
3+
import { formDataToObject, isEqual } from "./helpers"
4+
import { Signal, signal } from "./signals"
25

3-
export type Action<E extends HTMLElement, T> = (
6+
export type Action<E extends HTMLElement, T, R = {}> = (
47
node: E,
58
parameters?: T
69
) => {
710
update?: (parameters: T) => void
811
destroy?: () => void
9-
}
12+
} & R
1013

1114
type PrefetchMethod = 'click' | 'mount' | 'hover'
1215
type PrefetchLinkConfig = {
@@ -153,3 +156,50 @@ export const visible: Action<HTMLElement, (VisibleConfig & RouterRequestConfig)
153156
}
154157
}
155158
}
159+
160+
interface FormOptions extends RouterRequestConfig {
161+
before?: (form: VortexForm<any>) => VortexForm<any>
162+
after?: (result: Promise<RouterResponse<any>>) => any
163+
}
164+
165+
export const form: Action<HTMLFormElement,FormOptions,{ errors: Signal<Record<string, string>> }> = (node, rawOptions = {}) => {
166+
let options: FormOptions = {
167+
before: (form) => form,
168+
after: (result) => result,
169+
...rawOptions
170+
}
171+
const form = useForm(formDataToObject(new FormData(node)))
172+
const errors = signal(form.get().errors, isEqual)
173+
174+
const unsubscribe = form.subscribe((form) => errors.set(form.errors))
175+
176+
function submit(event: SubmitEvent) {
177+
event.preventDefault()
178+
179+
const { before, after } = options
180+
181+
// @ts-expect-error
182+
return after(before(form.get()).request({
183+
method: node.method || 'post',
184+
url: node.action || (window.location.origin + window.location.pathname),
185+
...options
186+
}))
187+
}
188+
189+
node.addEventListener('submit', submit)
190+
191+
return {
192+
errors,
193+
update(newOptions) {
194+
options = {
195+
before: (form) => form,
196+
after: (result) => result,
197+
...newOptions
198+
}
199+
},
200+
destroy() {
201+
node.removeEventListener('submit', submit)
202+
unsubscribe()
203+
}
204+
}
205+
}

src/helpers.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,68 @@ export function proxyUnwrap<T>(data: T): T {
4040
acc[key] = proxyUnwrap(data[key]);
4141
return acc
4242
}, {} as T)
43-
}
43+
}
44+
45+
export function formDataToObject(formData: FormData): Record<string, unknown> {
46+
const obj = {};
47+
48+
function assign(obj, path, value) {
49+
let current = obj;
50+
51+
for (let i = 0; i < path.length; i++) {
52+
const key = path[i];
53+
const isLast = i === path.length - 1;
54+
55+
if (isLast) {
56+
// type cast
57+
if (value === 'true') value = true;
58+
else if (value === 'false') value = false;
59+
else if (!isNaN(value) && value.trim() !== '') value = +value;
60+
61+
if (Array.isArray(current) && key === '') current.push(value);
62+
else current[key] = value;
63+
} else {
64+
const nextKey = path[i + 1];
65+
if (!(key in current)) {
66+
if (nextKey === '' || !isNaN(nextKey)) current[key] = [];
67+
else current[key] = {};
68+
}
69+
current = current[key];
70+
}
71+
}
72+
}
73+
74+
for (const [fullKey, value] of formData.entries()) {
75+
const path: string[] = [];
76+
let buffer = '';
77+
let bracketMode = false;
78+
let escapeNext = false;
79+
80+
for (let char of fullKey) {
81+
if (escapeNext) {
82+
buffer += char;
83+
escapeNext = false;
84+
} else if (char === '\\') {
85+
escapeNext = true;
86+
} else if (char === '.' && !bracketMode) {
87+
if (buffer) path.push(buffer);
88+
buffer = '';
89+
} else if (char === '[') {
90+
if (buffer) path.push(buffer);
91+
buffer = '';
92+
bracketMode = true;
93+
} else if (char === ']' && bracketMode) {
94+
path.push(buffer);
95+
buffer = '';
96+
bracketMode = false;
97+
} else {
98+
buffer += char;
99+
}
100+
}
101+
if (buffer) path.push(buffer);
102+
103+
assign(obj, path, value);
104+
}
105+
106+
return obj;
107+
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from "./client";
22
export * from "./page";
3-
export { link, visible } from "./dom";
3+
export { link, visible, form } from "./dom";
44
export { usePoll } from "./polling";
55
export { useForm } from "./form";
66
export { useRemember } from "./remember";

0 commit comments

Comments
 (0)