taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/web-util/src/forms/FormProvider.tsx | 1+
Mpackages/web-util/src/forms/forms-ui.tsx | 26+++++++++++++++++---------
Mpackages/web-util/src/forms/forms-utils.ts | 37++++++++++++++-----------------------
Mpackages/web-util/src/hooks/useForm.ts | 35+++++++++++++++--------------------
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: {