commit dfcb1700f50a72a38a1164a204737b1178f5e6cd
parent 3a2ac2ea4b14f7815ad29cfef3d5826766fbd5ad
Author: Florian Dold <florian@dold.me>
Date: Sat, 22 Mar 2025 21:09:26 +0100
forms: have one handler per UI field, not per form attribute
Diffstat:
4 files changed, 47 insertions(+), 52 deletions(-)
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
@@ -62,6 +62,7 @@ export interface UIFormProps<ValType> {
}
export type UIFieldHandler<T = any> = {
+ name: string;
value: T | undefined;
onChange: (s: T) => void;
error?: TranslatedString;
diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx
@@ -2,7 +2,7 @@ import { h as create, Fragment, h, VNode } from "preact";
import {
ErrorAndLabel,
FormErrors,
- FormHandler,
+ FormFieldStateMap,
useForm,
} from "../hooks/useForm.js";
// import { getConverterById, useTranslationContext } from "../index.browser.js";
@@ -31,14 +31,14 @@ export function DefaultForm<T>({
return (
<div>
- <hr class="mt-3 mb-3"/>
+ <hr class="mt-3 mb-3" />
<FormUI design={design} handler={handler} />
- <hr class="mt-3 mb-3"/>
+ <hr class="mt-3 mb-3" />
<p>Result JSON:</p>
<pre class="break-all whitespace-pre-wrap">
{JSON.stringify(status.result ?? {}, undefined, 2)}
</pre>
- <hr class="mt-3 mb-3"/>
+ <hr class="mt-3 mb-3" />
{status.status !== "ok" ? (
<ErrorsSummary errors={status.errors} />
) : undefined}
@@ -59,7 +59,7 @@ export function FormUI<T>({
}: {
name?: string;
design: FormDesign;
- handler: FormHandler<T>;
+ handler: FormFieldStateMap;
focus?: boolean;
}): VNode {
switch (design.type) {
@@ -68,6 +68,7 @@ export function FormUI<T>({
if (!section) return <Fragment />;
return (
<DoubleColumnFormSectionUI
+ sectionKey={i}
key={i}
name={name}
section={section}
@@ -92,18 +93,25 @@ export function FormUI<T>({
}
export function DoubleColumnFormSectionUI<T>({
+ sectionKey,
section,
name,
focus,
handler,
}: {
+ sectionKey: number | string;
name: string;
- handler: FormHandler<T>;
+ handler: FormFieldStateMap;
section: DoubleColumnFormSection;
focus?: boolean;
}): VNode {
const { i18n } = useTranslationContext();
- const fs = convertFormConfigToUiField(i18n, section.fields, handler);
+ const fs = convertFormConfigToUiField(
+ i18n,
+ sectionKey,
+ section.fields,
+ handler,
+ );
const allHidden = fs.every((v) => {
// FIXME: Handler should probably be present for all Form UI fields, not just some.
if ("handler" in v.properties) {
@@ -147,7 +155,7 @@ export function SingleColumnFormSectionUI<T>({
focus,
}: {
name: string;
- handler: FormHandler<T>;
+ handler: FormFieldStateMap;
fields: UIFormElementConfig[];
focus?: boolean;
}): VNode {
@@ -160,7 +168,7 @@ export function SingleColumnFormSectionUI<T>({
<div class="p-3">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
- fields={convertFormConfigToUiField(i18n, fields, handler)}
+ fields={convertFormConfigToUiField(i18n, `root`, fields, handler)}
focus={focus}
/>
</div>
diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts
@@ -6,6 +6,7 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import { format, parse } from "date-fns";
+import { FormFieldStateMap } from "../hooks/useForm.js";
import {
InternationalizationAPI,
UIFieldElementDescription,
@@ -25,11 +26,11 @@ import { UIFormElementConfig, UIFormFieldBaseConfig } from "./forms-types.js";
*/
export function convertFormConfigToUiField(
i18n_: InternationalizationAPI,
+ parentKey: string | number,
fieldConfig: UIFormElementConfig[],
- // FIXME: Clarify types, this is a FormHandler
- form: object,
+ form: FormFieldStateMap,
): UIFormField[] {
- const result = fieldConfig.map((config) => {
+ const result = fieldConfig.map((config, fieldIndex) => {
if (config.type === "void") return undefined;
// NON input fields
switch (config.type) {
@@ -79,16 +80,21 @@ export function convertFormConfigToUiField(
type: config.type,
properties: {
...converBaseFieldsProps(i18n_, config),
- fields: convertFormConfigToUiField(i18n_, config.fields, form),
+ fields: convertFormConfigToUiField(
+ i18n_,
+ `${parentKey}.${fieldIndex}`,
+ config.fields,
+ form,
+ ),
},
};
return resp;
}
}
- const names = config.id.split(".");
- const handler = getValueDeeper2(form, names);
- const name = names[names.length - 1];
- //FIXME: first computed prop, all should be computed
+ const uiKey = `${parentKey}.${fieldIndex}`;
+ const handler = form[uiKey];
+ const name = handler.name;
+ // FIXME: first computed prop, all should be computed
const hidden =
config.hidden === true
? true
@@ -418,21 +424,6 @@ function converBaseFieldsProps(
};
}
-function getValueDeeper2(
- object: Record<string, any>,
- names: string[],
-): UIFieldHandler {
- if (names.length === 0) return object as UIFieldHandler;
- const [head, ...rest] = names;
- if (!head) {
- return getValueDeeper2(object, rest);
- }
- if (object === undefined) {
- throw Error("handler not found");
- }
- return getValueDeeper2(object[head], rest);
-}
-
const nullConverter: StringConverter<string> = {
fromStringUI(v: string | undefined): string {
return v ?? "";
diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts
@@ -31,18 +31,9 @@ import {
} from "../index.browser.js";
/**
- * T is the type of the form's result content.
- * Every primitive type is converted to a form handler.
+ * Mapping from the key of a form field to the state of the form field.
*/
-export type FormHandler<T> = {
- [k in keyof T]?: T[k] extends string
- ? UIFieldHandler
- : T[k] extends AmountJson
- ? UIFieldHandler
- : T[k] extends TalerExchangeApi.AmlState
- ? UIFieldHandler
- : FormHandler<T[k]>;
-};
+export type FormFieldStateMap = { [x: string]: UIFieldHandler };
export type FormValues<T> = {
[k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
@@ -88,7 +79,7 @@ export type FormStatus<T> =
};
export type FormState<T> = {
- handler: FormHandler<T>;
+ handler: FormFieldStateMap;
status: FormStatus<T>;
update: (f: FormValues<T>) => void;
};
@@ -226,17 +217,18 @@ function constructFormHandler<T>(
onValueChange: (d: RecursivePartial<FormValues<T>>) => void,
i18n: InternationalizationAPI,
): {
- handler: FormHandler<T>;
+ handler: FormFieldStateMap;
result: FormStatus<T>;
errors: FormErrors<T> | undefined;
} {
- let handler: FormHandler<T> = {};
+ let handler: FormFieldStateMap = {};
let result = {} as FormStatus<T>;
let errors: FormErrors<T> | undefined = undefined;
function createFieldHandler(
formElement: UIFormElementConfig,
hiddenSection: boolean | undefined,
+ handlerUiPath: string,
): void {
if (!("id" in formElement)) {
return;
@@ -269,6 +261,7 @@ function constructFormHandler<T>(
}
const field: UIFieldHandler = {
+ name: formElement.id,
error: currentError?.message,
value: currentValue,
onChange: updater,
@@ -278,9 +271,7 @@ function constructFormHandler<T>(
},
};
- // FIXME: handler should not be set but we also need to refactor
- // ui components.
- handler = setValueIntoPath(handler, path, field);
+ handler[handlerUiPath] = field;
if (!hidden) {
result = setValueIntoPath(result, path, field.value) ?? {};
}
@@ -288,14 +279,18 @@ function constructFormHandler<T>(
switch (design.type) {
case "double-column": {
- design.sections.forEach((sec) => {
+ design.sections.forEach((sec, secIndex) => {
const hidden = sec.hide && sec.hide(result);
- sec.fields.forEach((f) => createFieldHandler(f, hidden));
+ sec.fields.forEach((f, fieldIndex) =>
+ createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`),
+ );
});
break;
}
case "single-column": {
- design.fields.forEach((f) => createFieldHandler(f, undefined));
+ design.fields.forEach((f, fieldIndex) =>
+ createFieldHandler(f, undefined, `${fieldIndex}`),
+ );
break;
}
default: {