gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/04: remove duplicated form implementation


From: gnunet
Subject: [taler-wallet-core] 01/04: remove duplicated form implementation
Date: Fri, 10 Jan 2025 19:54:27 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit b1090b6b3d4b725e05da426b523578c1603e7c6f
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Jan 10 15:42:28 2025 -0300

    remove duplicated form implementation
---
 packages/web-util/src/forms/DefaultForm.tsx        | 105 ------
 packages/web-util/src/forms/FormProvider.tsx       |  41 +-
 packages/web-util/src/forms/Group.tsx              |  12 +-
 .../src/forms/InputAbsoluteTime.stories.tsx        |  15 +-
 packages/web-util/src/forms/InputAbsoluteTime.tsx  |   7 +-
 .../web-util/src/forms/InputAmount.stories.tsx     |  14 +-
 packages/web-util/src/forms/InputAmount.tsx        |   5 +-
 packages/web-util/src/forms/InputArray.stories.tsx |  24 +-
 packages/web-util/src/forms/InputArray.tsx         |  98 ++---
 .../src/forms/InputChoiceHorizontal.stories.tsx    |  14 +-
 .../web-util/src/forms/InputChoiceHorizontal.tsx   |  11 +-
 .../src/forms/InputChoiceStacked.stories.tsx       |  14 +-
 packages/web-util/src/forms/InputChoiceStacked.tsx |   2 -
 packages/web-util/src/forms/InputFile.stories.tsx  |  22 +-
 packages/web-util/src/forms/InputFile.tsx          |  14 +-
 .../web-util/src/forms/InputInteger.stories.tsx    |  18 +-
 packages/web-util/src/forms/InputLine.stories.tsx  |  65 ----
 packages/web-util/src/forms/InputLine.tsx          |   1 -
 .../src/forms/InputSelectMultiple.stories.tsx      |  16 +-
 .../web-util/src/forms/InputSelectMultiple.tsx     |  16 +-
 .../web-util/src/forms/InputSelectOne.stories.tsx  |  14 +-
 packages/web-util/src/forms/InputSelectOne.tsx     |   8 +-
 packages/web-util/src/forms/InputText.stories.tsx  |  14 +-
 .../web-util/src/forms/InputTextArea.stories.tsx   |  16 +-
 .../web-util/src/forms/InputToggle.stories.tsx     |  53 +--
 packages/web-util/src/forms/InputToggle.tsx        |   8 +-
 packages/web-util/src/forms/converter.ts           | 130 -------
 packages/web-util/src/forms/field-types.ts         | 117 ++++++
 .../src/forms/{ui-form.ts => forms-types.ts}       |  49 ++-
 packages/web-util/src/forms/forms-ui.tsx           | 129 +++++++
 packages/web-util/src/forms/forms-utils.ts         | 382 +++++++++++++++++++
 packages/web-util/src/forms/forms.ts               | 411 ---------------------
 packages/web-util/src/forms/index.stories.ts       |   1 -
 packages/web-util/src/forms/index.ts               |  49 ++-
 packages/web-util/src/forms/useField.ts            |  30 +-
 packages/web-util/src/hooks/index.ts               |   8 +-
 packages/web-util/src/hooks/useForm.ts             | 315 ++++++++--------
 packages/web-util/src/stories.html                 |   6 +-
 38 files changed, 1040 insertions(+), 1214 deletions(-)

diff --git a/packages/web-util/src/forms/DefaultForm.tsx 
b/packages/web-util/src/forms/DefaultForm.tsx
deleted file mode 100644
index 239577e24..000000000
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import {
-  UIFormElementConfig,
-  getConverterById,
-  useTranslationContext,
-} from "../index.browser.js";
-import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
-import {
-  RenderAllFieldsByUiConfig,
-  UIFormField,
-  convertUiField,
-} from "./forms.js";
-// import { FlexibleForm } from "./ui-form.js";
-
-/**
- * Flexible form uses a DoubleColumForm for design
- * and may have a dynamic properties defined by
- * behavior function.
- */
-export interface FlexibleForm_Deprecated<T extends object> {
-  design: DoubleColumnForm_Deprecated;
-  behavior?: (form: Partial<T>) => FormState<T>;
-}
-
-/**
- * Double column form
- *
- * Form with sections, every sections have a title and may
- * have a description.
- * Every sections contain a set of fields.
- */
-export type DoubleColumnForm_Deprecated = Array<
-  DoubleColumnFormSection_Deprecated | undefined
->;
-
-export type DoubleColumnFormSection_Deprecated = {
-  title: TranslatedString;
-  description?: TranslatedString;
-  fields: UIFormElementConfig[];
-};
-
-/**
- * Form Provider implementation that use FlexibleForm
- * to defined behavior and fields.
- */
-export function DefaultForm<T extends object>({
-  initial,
-  onUpdate,
-  form,
-  onSubmit,
-  children,
-  readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & {
-  form: FlexibleForm_Deprecated<T>;
-}): VNode {
-  const { i18n } = useTranslationContext();
-  return (
-    <FormProvider
-      initial={initial}
-      onUpdate={onUpdate}
-      onSubmit={onSubmit}
-      readOnly={readOnly}
-    >
-      <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
-        {form.design.map((section, i) => {
-          if (!section) return <Fragment />;
-          return (
-            <div
-              key={i}
-              class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
-            >
-              <div class="px-4 sm:px-0">
-                <h2 class="text-base font-semibold leading-7 text-gray-900">
-                  {section.title}
-                </h2>
-                {section.description && (
-                  <p class="mt-1 text-sm leading-6 text-gray-600">
-                    {section.description}
-                  </p>
-                )}
-              </div>
-              <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
md:col-span-2">
-                <div class="p-3">
-                  <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-                    <RenderAllFieldsByUiConfig
-                      key={i}
-                      fields={convertUiField(
-                        i18n,
-                        section.fields,
-                        form,
-                        getConverterById,
-                      )}
-                    />
-                  </div>
-                </div>
-              </div>
-            </div>
-          );
-        })}
-      </div>
-      {children}
-    </FormProvider>
-  );
-}
diff --git a/packages/web-util/src/forms/FormProvider.tsx 
b/packages/web-util/src/forms/FormProvider.tsx
index fe886030a..57baa9a39 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -78,9 +78,9 @@ export interface UIFormProps<T extends object, K extends 
keyof T>
   handler?: UIFieldHandler;
 }
 
-export type UIFieldHandler = {
-  value: string | undefined;
-  onChange: (s: string) => void;
+export type UIFieldHandler<T = any> = {
+  value: T | undefined;
+  onChange: (s: T) => void;
   state: FieldUIOptions;
   error?: TranslatedString;
 };
@@ -113,38 +113,3 @@ export type FormProviderProps<T extends object> = 
Omit<FormType<T>, "value"> & {
   onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
   children?: ComponentChildren;
 };
-
-export function FormProvider<T extends object>({
-  children,
-  initial,
-  onUpdate: notify,
-  onSubmit,
-  computeFormState,
-  readOnly,
-}: FormProviderProps<T>): VNode {
-  const [state, setState] = useState<Partial<T>>(initial ?? {});
-  const value = { current: state };
-  const onUpdate = (v: typeof state) => {
-    setState(v);
-    if (notify) notify(v);
-  };
-  return (
-    <FormContext.Provider
-      value={{ initial, value, onUpdate, computeFormState, readOnly }}
-    >
-      <form
-        onSubmit={(e) => {
-          e.preventDefault();
-          //@ts-ignore
-          if (onSubmit)
-            onSubmit(
-              value.current,
-              !computeFormState ? undefined : computeFormState(value.current),
-            );
-        }}
-      >
-        {children}
-      </form>
-    </FormContext.Provider>
-  );
-}
diff --git a/packages/web-util/src/forms/Group.tsx 
b/packages/web-util/src/forms/Group.tsx
index f63fa4a9b..83504fc96 100644
--- a/packages/web-util/src/forms/Group.tsx
+++ b/packages/web-util/src/forms/Group.tsx
@@ -1,11 +1,9 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { VNode, h } from "preact";
+import { Addon } from "./FormProvider.js";
 import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField, convertUiField } from 
"./forms.js";
-import { Addon, FormProvider } from "./FormProvider.js";
-import { useField } from "./useField.js";
-import { useTranslationContext } from "../index.browser.js";
-import { getConverterById } from "./converter.js";
+import { RenderAllFieldsByUiConfig } from "./forms-ui.js";
+import { UIFormField } from "./field-types.js";
 
 interface Props {
   label: TranslatedString;
@@ -35,9 +33,7 @@ export function Group({
         </p>
       )}
       <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
-        <RenderAllFieldsByUiConfig 
-          fields={fields}
-          />
+        <RenderAllFieldsByUiConfig fields={fields} />
       </div>
     </div>
   );
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx 
b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 858349a00..28afd71b1 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -21,12 +21,8 @@
 
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
-
+import { FormDesign, UIHandlerId } from "./forms-types.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
 export default {
   title: "Input Absolute Time",
 };
@@ -44,8 +40,9 @@ const initial: TargetObject = {
   today: AbsoluteTime.now(),
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -62,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx 
b/packages/web-util/src/forms/InputAbsoluteTime.tsx
index f5fd4fc50..a1287cc9b 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx
@@ -6,7 +6,6 @@ import { Calendar } from "./Calendar.js";
 import { Dialog } from "./Dialog.js";
 import { UIFormProps } from "./FormProvider.js";
 import { InputLine } from "./InputLine.js";
-import { useField } from "./useField.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 
 export function InputAbsoluteTime<T extends object, K extends keyof T>(
@@ -15,10 +14,8 @@ export function InputAbsoluteTime<T extends object, K 
extends keyof T>(
   const pattern = properties.pattern ?? "dd/MM/yyyy";
   const [open, setOpen] = useState(false);
 
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(properties.name);
   const { value, onChange } =
-    properties.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(properties.name);
+    properties.handler ?? noHandlerPropsAndNoContextForField(properties.name);
   return (
     <Fragment>
       <InputLine<T, K>
@@ -71,7 +68,7 @@ export function InputAbsoluteTime<T extends object, K extends 
keyof T>(
       {open && (
         <Dialog onClose={() => setOpen(false)}>
           <Calendar
-            value={(value as AbsoluteTime) ?? AbsoluteTime.now()}
+            value={(value as any as AbsoluteTime) ?? AbsoluteTime.now()}
             onChange={(v) => {
               onChange(v as any);
               setOpen(false);
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx 
b/packages/web-util/src/forms/InputAmount.stories.tsx
index 4351a9655..18516db14 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -21,11 +21,8 @@
 
 import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Amount",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
   amount: Amounts.parseOrThrow("USD:10"),
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -62,5 +60,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputAmount.tsx 
b/packages/web-util/src/forms/InputAmount.tsx
index 647d2c823..874fa8686 100644
--- a/packages/web-util/src/forms/InputAmount.tsx
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -2,16 +2,13 @@ import { AmountJson, Amounts, TranslatedString } from 
"@gnu-taler/taler-util";
 import { VNode, h } from "preact";
 import { UIFormProps } from "./FormProvider.js";
 import { InputLine } from "./InputLine.js";
-import { useField } from "./useField.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 
 export function InputAmount<T extends object, K extends keyof T>(
   props: { currency?: string } & UIFormProps<T, K>,
 ): VNode {
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
   const { value } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
   const currency =
     !value || !(value as any).currency
       ? props.currency
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx 
b/packages/web-util/src/forms/InputArray.stories.tsx
index 6f478fd07..e9807d2e6 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Array",
@@ -52,8 +49,9 @@ const initial: TargetObject = {
   ],
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       description: "to test how arrays are used" as TranslatedString,
@@ -85,14 +83,14 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const FormWithArray = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
 
-const initial2: any = {
-};
+const initial2: any = {};
 
-const form2: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design2: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "Personal information" as TranslatedString,
       fields: [
@@ -135,5 +133,5 @@ const form2: FlexibleForm_Deprecated<TargetObject> = {
 
 export const NonMixingProperties = tests.createExample(TestedComponent, {
   initial: initial2,
-  form: form2,
+  design: design2,
 });
diff --git a/packages/web-util/src/forms/InputArray.tsx 
b/packages/web-util/src/forms/InputArray.tsx
index 60a13afae..f1fd00b11 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -1,23 +1,14 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
+import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
-import { FormProvider, UIFormProps } from "./FormProvider.js";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import {
-  convertUiField,
-  RenderAllFieldsByUiConfig,
-  UIFormField,
-} from "./forms.js";
-import { useField } from "./useField.js";
-import { UIFormElementConfig, UIHandlerId } from "./ui-form.js";
+import { getValueFromPath, useForm } from "../hooks/useForm.js";
 import {
-  FormErrors,
-  undefinedIfEmpty,
-  useFormState,
-  useFormStateFromConfig,
-  validateRequiredFields,
-} from "../hooks/useForm.js";
-import { getConverterById, useTranslationContext } from "../index.browser.js";
+  SingleColumnFormSectionUI,
+  useTranslationContext,
+} from "../index.browser.js";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { UIFormElementConfig, UIHandlerId } from "./forms-types.js";
 
 function Option({
   label,
@@ -192,47 +183,20 @@ export function InputArray<T extends object, K extends 
keyof T>(
   const selected =
     selectedIndex === undefined ? undefined : list[selectedIndex];
 
-  // const shape: Array<UIHandlerId> = [];
-  // const requiredFields: Array<UIHandlerId> = [];
-  // Array.prototype.push.apply(shape, getShapeFromFields(fields));
-  // Array.prototype.push.apply(requiredFields, getRequiredFields(fields));
-
-  const [form, formState] = useFormStateFromConfig<FormType>(
-    fields,
+  const form = useForm<FormType>(
+    {
+      type: "single-column",
+      fields,
+    },
     selected ?? {},
   );
-  // const [form, formState] = useFormState<FormType>(
-  //   shape,
-  //   selected ?? {},
-  //   (st) => {
-  //     const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({});
-
-  //     const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
-  //       validateRequiredFields(partialErrors, st, requiredFields),
-  //     );
-
-  //     if (errors === undefined) {
-  //       return {
-  //         status: "ok",
-  //         result: st as any,
-  //         errors: undefined,
-  //       };
-  //     }
-
-  //     return {
-  //       status: "fail",
-  //       result: st as any,
-  //       errors,
-  //     };
-  //   },
-  // );
 
   useEffect(() => {
     if (selectedIndex === undefined) return;
     const newValue = [...list];
-    newValue.splice(selectedIndex, 1, formState.result);
+    newValue.splice(selectedIndex, 1, form.status.result);
     onChange(newValue as any);
-  }, [formState.result, selectedIndex]);
+  }, [form.status.result, selectedIndex]);
   const { i18n } = useTranslationContext();
 
   return (
@@ -247,7 +211,7 @@ export function InputArray<T extends object, K extends 
keyof T>(
         <div class="-space-y-px rounded-md bg-white ">
           {list.map((v, idx) => {
             const label =
-              getValueDeeper(v, labelField.split(".")) ?? "<<incomplete>>";
+              getValueFromPath(v, labelField.split(".")) ?? "<<incomplete>>";
             return (
               <Option
                 label={label as TranslatedString}
@@ -312,9 +276,18 @@ export function InputArray<T extends object, K extends 
keyof T>(
           // >
           <div class="px-4 py-6">
             <div class="grid grid-cols-1 gap-y-8 ">
-              <RenderAllFieldsByUiConfig
-                fields={convertUiField(i18n, fields, form, getConverterById)}
+              <SingleColumnFormSectionUI
+                fields={fields}
+                handler={form.handler}
               />
+              {/* <RenderAllFieldsByUiConfig
+                fields={convertUiField(
+                  i18n,
+                  fields,
+                  form.handler,
+                  getConverterById,
+                )}
+              /> */}
             </div>
           </div>
           // </FormProvider>
@@ -350,20 +323,3 @@ export function InputArray<T extends object, K extends 
keyof T>(
     </div>
   );
 }
-
-export function getValueDeeper(
-  object: Record<string, any>,
-  names: string[],
-): string {
-  if (names.length === 0) {
-    return object as any as string;
-  }
-  const [head, ...rest] = names;
-  if (!head) {
-    return getValueDeeper(object, rest);
-  }
-  if (object === undefined) {
-    return "";
-  }
-  return getValueDeeper(object[head], rest);
-}
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx 
b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index a00bcd6a1..f2c86779a 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Choice Horizontal",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
   comment: "0",
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -75,5 +73,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx 
b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
index 86d3aa926..bc50b59ed 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, VNode, h } from "preact";
 import { UIFormProps } from "./FormProvider.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 
 export interface ChoiceH<V> {
@@ -16,10 +15,8 @@ export function InputChoiceHorizontal<T extends object, K 
extends keyof T>(
   } & UIFormProps<T, K>,
 ): VNode {
   const { choices, label, tooltip, help, required, converter } = props;
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
   const { value, onChange, state } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
   if (state.hidden) {
     return <Fragment />;
   }
@@ -34,7 +31,7 @@ export function InputChoiceHorizontal<T extends object, K 
extends keyof T>(
       <fieldset class="mt-2">
         <div class="isolate inline-flex rounded-md shadow-sm">
           {choices.map((choice, idx) => {
-            const convertedValue = converter?.fromStringUI(choice.value as any)
+            const convertedValue = converter?.fromStringUI(choice.value as 
any);
             const isFirst = idx === 0;
             const isLast = idx === choices.length - 1;
             let clazz =
@@ -62,7 +59,9 @@ export function InputChoiceHorizontal<T extends object, K 
extends keyof T>(
                 class={clazz}
                 onClick={(e) => {
                   onChange(
-                    (value === choice.value ? undefined : convertedValue) as 
any,
+                    (value === choice.value
+                      ? undefined
+                      : convertedValue) as any,
                   );
                 }}
               >
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx 
b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index 6e6a1a126..d08069893 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Choice Stacked",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
   comment: "some initial comment",
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -75,5 +73,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx 
b/packages/web-util/src/forms/InputChoiceStacked.tsx
index 34b06ec0c..2415cd5db 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, VNode, h } from "preact";
 import { UIFormProps } from "./FormProvider.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 
 export interface ChoiceS<V> {
@@ -29,7 +28,6 @@ export function InputChoiceStacked<T extends object, K 
extends keyof T>(
     converter,
   } = props;
 
-  //FIXME: remove deprecated
   const { value, onChange, state } =
     props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx 
b/packages/web-util/src/forms/InputFile.stories.tsx
index 75b1fb918..24f983ad4 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input File",
@@ -38,14 +35,15 @@ export namespace Simplest {
 }
 
 type TargetObject = {
-  comment: string;
+  file?: string;
 };
 const initial: TargetObject = {
-  comment: "some initial comment",
+  file: undefined,
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -53,7 +51,7 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
           type: "file",
           label: "label of the field" as TranslatedString,
           required: true,
-          id: "comment" as UIHandlerId,
+          id: "file" as UIHandlerId,
           accept: ".png",
           tooltip:
             "this is a very long tooltip that explain what the field does 
without being short" as TranslatedString,
@@ -64,7 +62,7 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
   ],
 };
 
-export const SimpleComment = tests.createExample(TestedComponent, {
+export const AcceptPNG = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputFile.tsx 
b/packages/web-util/src/forms/InputFile.tsx
index cd0a96d1c..78eb9cf29 100644
--- a/packages/web-util/src/forms/InputFile.tsx
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -2,23 +2,13 @@ import { Fragment, VNode, h } from "preact";
 import { UIFormProps } from "./FormProvider.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
 
 export function InputFile<T extends object, K extends keyof T>(
   props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
 ): VNode {
-  const {
-    label,
-    tooltip,
-    required,
-    help: propsHelp,
-    maxBites,
-    accept,
-  } = props;
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
+  const { label, tooltip, required, help: propsHelp, maxBites, accept } = 
props;
   const { value, onChange, state } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
   const help = propsHelp ?? state.help;
   if (state.hidden) {
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx 
b/packages/web-util/src/forms/InputInteger.stories.tsx
index 76d9e8668..369be670d 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Integer",
@@ -38,15 +35,16 @@ const initial: TargetObject = {
   age: 5,
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
           type: "integer",
-          label: "label of the field" as TranslatedString,
-          id: "comment" as UIHandlerId,
+          label: "Age" as TranslatedString,
+          id: "age" as UIHandlerId,
           tooltip: "just numbers" as TranslatedString,
         },
       ],
@@ -56,5 +54,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx 
b/packages/web-util/src/forms/InputLine.stories.tsx
deleted file mode 100644
index e5209f4d4..000000000
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
-
-export default {
-  title: "Input Line",
-};
-
-export namespace Simplest {
-  export interface Form {
-    comment: string;
-  }
-}
-
-type TargetObject = {
-  comment: string;
-};
-const initial: TargetObject = {
-  comment: "some initial comment",
-};
-
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
-    {
-      title: "this is a simple form" as TranslatedString,
-      fields: [
-        {
-          type: "text",
-          label: "label of the field" as TranslatedString,
-          id: "comment" as UIHandlerId,
-        },
-      ],
-    },
-  ],
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
-  initial,
-  form,
-});
diff --git a/packages/web-util/src/forms/InputLine.tsx 
b/packages/web-util/src/forms/InputLine.tsx
index 7eceea88e..6f160abf4 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
 import { Addon, UIFormProps } from "./FormProvider.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
-import { useField } from "./useField.js";
 
 //@ts-ignore
 const TooltipIcon = (
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx 
b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index 9cb997490..a39f5fb26 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Select Multiple",
@@ -46,14 +43,15 @@ const initial: TargetObject = {
   things: [],
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
           type: "selectMultiple",
-          label: "allow diplicates" as TranslatedString,
+          label: "allow duplicates" as TranslatedString,
           id: "pets" as UIHandlerId,
           placeholder: "search..." as TranslatedString,
           choices: [
@@ -99,5 +97,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx 
b/packages/web-util/src/forms/InputSelectMultiple.tsx
index 1bcf85061..1a52ce9d1 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -4,7 +4,6 @@ import { UIFormProps } from "./FormProvider.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 import { ChoiceS } from "./InputChoiceStacked.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
 
 export function InputSelectMultiple<T extends object, K extends keyof T>(
   props: {
@@ -13,11 +12,18 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
     max?: number;
   } & UIFormProps<T, K>,
 ): VNode {
-  const { converter, label, choices, placeholder, tooltip, required, unique, 
max } = props;
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
+  const {
+    converter,
+    label,
+    choices,
+    placeholder,
+    tooltip,
+    required,
+    unique,
+    max,
+  } = props;
   const { value, onChange, state } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
   const [filter, setFilter] = useState<string | undefined>(undefined);
   const regex = new RegExp(`.*${filter}.*`, "i");
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx 
b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index 25b96f0c0..ba14d180d 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Select One",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
   things: "one",
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -76,5 +74,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx 
b/packages/web-util/src/forms/InputSelectOne.tsx
index 26f887b08..331f4720b 100644
--- a/packages/web-util/src/forms/InputSelectOne.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -1,10 +1,9 @@
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 import { ChoiceS } from "./InputChoiceStacked.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 
 export function InputSelectOne<T extends object, K extends keyof T>(
   props: {
@@ -12,11 +11,8 @@ export function InputSelectOne<T extends object, K extends 
keyof T>(
   } & UIFormProps<T, K>,
 ): VNode {
   const { label, choices, placeholder, tooltip, required } = props;
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
   const { value, onChange } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
-
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
   const [filter, setFilter] = useState<string | undefined>(undefined);
   const regex = new RegExp(`.*${filter}.*`, "i");
diff --git a/packages/web-util/src/forms/InputText.stories.tsx 
b/packages/web-util/src/forms/InputText.stories.tsx
index 6d0db938b..e4ea621a2 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Text",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
   comment: "some initial comment",
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
@@ -61,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx 
b/packages/web-util/src/forms/InputTextArea.stories.tsx
index a3b135c36..5d3cf4f12 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  DefaultForm as TestedComponent,
-  FlexibleForm_Deprecated,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Text Area",
@@ -44,13 +41,14 @@ const initial: TargetObject = {
   comment: "some initial comment",
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
+const design: FormDesign = {
+  type: "double-column",
+  sections: [
     {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
-          type: "text",
+          type: "textArea",
           label: "label of the field" as TranslatedString,
           id: "comment" as UIHandlerId,
         },
@@ -61,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
 
 export const SimpleComment = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design,
 });
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx 
b/packages/web-util/src/forms/InputToggle.stories.tsx
index 75a81edd3..c4ed868ef 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -21,11 +21,8 @@
 
 import { TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../tests/hook.js";
-import {
-  FlexibleForm_Deprecated,
-  DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
 
 export default {
   title: "Input Toggle",
@@ -38,29 +35,37 @@ export namespace Simplest {
 }
 
 type TargetObject = {
-  comment: string;
+  accept: boolean;
 };
 const initial: TargetObject = {
-  comment: "some initial comment",
+  accept: true,
 };
 
-const form: FlexibleForm_Deprecated<TargetObject> = {
-  design: [
-    {
-      title: "this is a simple form" as TranslatedString,
-      fields: [
-        {
-          type: "toggle",
-          label: "label of the field" as TranslatedString,
-          threeState: false,
-          id: "comment" as UIHandlerId,
-        },
-      ],
-    },
-  ],
-};
+export const SimpleUsage = tests.createExample(TestedComponent, {
+  initial,
+  design: {
+    type: "single-column",
+    fields: [
+      {
+        type: "toggle",
+        label: "do you accept?" as TranslatedString,
+        id: "accept" as UIHandlerId,
+      },
+    ],
+  },
+});
 
-export const SimpleComment = tests.createExample(TestedComponent, {
+export const WithThreeState = tests.createExample(TestedComponent, {
   initial,
-  form,
+  design: {
+    type: "single-column",
+    fields: [
+      {
+        type: "toggle",
+        label: "do you accept?" as TranslatedString,
+        threeState: true,
+        id: "accept" as UIHandlerId,
+      },
+    ],
+  },
 });
diff --git a/packages/web-util/src/forms/InputToggle.tsx 
b/packages/web-util/src/forms/InputToggle.tsx
index d310bc0f2..b1c800ad3 100644
--- a/packages/web-util/src/forms/InputToggle.tsx
+++ b/packages/web-util/src/forms/InputToggle.tsx
@@ -2,7 +2,6 @@ import { VNode, h } from "preact";
 import { UIFormProps } from "./FormProvider.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
 
 export function InputToggle<T extends object, K extends keyof T>(
   props: { threeState: boolean } & UIFormProps<T, K>,
@@ -19,14 +18,13 @@ export function InputToggle<T extends object, K extends 
keyof T>(
     converter,
     threeState,
   } = props;
-  //FIXME: remove deprecated
-  const fieldCtx = useField<T, K>(props.name);
   const { value, onChange } =
-    props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
   const isOn = !!value;
   return (
     <div class="sm:col-span-6">
+      v = {JSON.stringify({ value, isOn })}
       <div class="flex items-center justify-between">
         <LabelWithTooltipMaybeRequired
           label={label}
@@ -51,7 +49,7 @@ export function InputToggle<T extends object, K extends keyof 
T>(
           <span
             data-state={isOn ? "on" : value === undefined ? "undefined" : 
"off"}
             class="translate-x-6 data-[state=off]:translate-x-0 
data-[state=undefined]:translate-x-3 pointer-events-none inline-block h-5 w-5 
transform rounded-full bg-white shadow ring-0 transition duration-200 
ease-in-out"
-            ></span>
+          ></span>
         </button>
       </div>
     </div>
diff --git a/packages/web-util/src/forms/converter.ts 
b/packages/web-util/src/forms/converter.ts
deleted file mode 100644
index eee891776..000000000
--- a/packages/web-util/src/forms/converter.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
-  AbsoluteTime,
-  AmountJson,
-  Amounts,
-  TalerExchangeApi,
-} from "@gnu-taler/taler-util";
-import { format, parse } from "date-fns";
-import { StringConverter } from "./FormProvider.js";
-
-export const amlStateConverter = {
-  toStringUI: stringifyAmlState,
-  fromStringUI: parseAmlState,
-};
-
-function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
-  if (s === undefined) return "";
-  switch (s) {
-    case TalerExchangeApi.AmlState.normal:
-      return "normal";
-    case TalerExchangeApi.AmlState.pending:
-      return "pending";
-    case TalerExchangeApi.AmlState.frozen:
-      return "frozen";
-  }
-}
-
-function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
-  switch (s) {
-    case "normal":
-      return TalerExchangeApi.AmlState.normal;
-    case "pending":
-      return TalerExchangeApi.AmlState.pending;
-    case "frozen":
-      return TalerExchangeApi.AmlState.frozen;
-    default:
-      throw Error(`unknown AML state: ${s}`);
-  }
-}
-
-const nullConverter: StringConverter<string> = {
-  fromStringUI(v: string | undefined): string {
-    return v ?? "";
-  },
-  toStringUI(v: unknown): string {
-    return v as string;
-  },
-};
-
-function amountConverter(config: any): StringConverter<AmountJson> {
-  const currency = config["currency"];
-  if (!currency || typeof currency !== "string") {
-    throw Error(`amount converter needs a currency`);
-  }
-  return {
-    fromStringUI(v: string | undefined): AmountJson {
-      // FIXME: requires currency
-      return (
-        Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency)
-      );
-    },
-    toStringUI(v: unknown): string {
-      return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
-    },
-  };
-}
-
-function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
-  const pattern = config["pattern"];
-  if (!pattern || typeof pattern !== "string") {
-    throw Error(`absTime converter needs a pattern`);
-  }
-  return {
-    fromStringUI(v: string | undefined): AbsoluteTime {
-      if (v === undefined) {
-        return AbsoluteTime.never();
-      }
-      try {
-        const time = parse(v, pattern, new Date());
-        return AbsoluteTime.fromMilliseconds(time.getTime());
-      } catch (e) {
-        return AbsoluteTime.never();
-      }
-    },
-    toStringUI(v: unknown): string {
-      if (v === undefined) return "";
-      const d = v as AbsoluteTime;
-      if (d.t_ms === "never") return "never";
-      try {
-        return format(d.t_ms, pattern);
-      } catch (e) {
-        return "";
-      }
-    },
-  };
-}
-
-export function getConverterById(
-  id: string | undefined,
-  config: unknown,
-): StringConverter<unknown> {
-  if (id === "Taler.AbsoluteTime") {
-    // @ts-expect-error check this
-    return absTimeConverter(config);
-  }
-  if (id === "Taler.Amount") {
-    // @ts-expect-error check this
-    return amountConverter(config);
-  }
-  if (id === "TalerExchangeApi.AmlState") {
-    // @ts-expect-error check this
-    return amlStateConverter;
-  }
-  return nullConverter as StringConverter<unknown>;
-}
diff --git a/packages/web-util/src/forms/field-types.ts 
b/packages/web-util/src/forms/field-types.ts
new file mode 100644
index 000000000..809467b24
--- /dev/null
+++ b/packages/web-util/src/forms/field-types.ts
@@ -0,0 +1,117 @@
+import { h as create, Fragment, VNode } from "preact";
+import { Caption } from "./Caption.js";
+import { Group } from "./Group.js";
+import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
+import { InputAmount } from "./InputAmount.js";
+import { InputArray } from "./InputArray.js";
+import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputFile } from "./InputFile.js";
+import { InputInteger } from "./InputInteger.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputSelectOne } from "./InputSelectOne.js";
+import { InputText } from "./InputText.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputToggle } from "./InputToggle.js";
+import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
+import {
+  InternationalizationAPI,
+  UIFieldElementDescription,
+} from "../index.browser.js";
+import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
+import { UIFormFieldBaseConfig, UIFormElementConfig } from "./forms-types.js";
+import { HtmlIframe } from "./HtmlIframe.js";
+import { DownloadLink } from "./DownloadLink.js";
+/**
+ * Constrain the type with the ui props
+ */
+type FieldType<T extends object = any, K extends keyof T = any> = {
+  group: Parameters<typeof Group>[0];
+  caption: Parameters<typeof Caption>[0];
+  "download-link": Parameters<typeof DownloadLink>[0];
+  htmlIframe: Parameters<typeof HtmlIframe>[0];
+  array: Parameters<typeof InputArray<T, K>>[0];
+  file: Parameters<typeof InputFile<T, K>>[0];
+  selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
+  selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
+  text: Parameters<typeof InputText<T, K>>[0];
+  textArea: Parameters<typeof InputTextArea<T, K>>[0];
+  choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
+  choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
+  absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
+  integer: Parameters<typeof InputInteger<T, K>>[0];
+  toggle: Parameters<typeof InputToggle<T, K>>[0];
+  amount: Parameters<typeof InputAmount<T, K>>[0];
+};
+
+/**
+ * List all the form fields so typescript can type-check the form instance
+ */
+export type UIFormField =
+  | { type: "group"; properties: FieldType["group"] }
+  | { type: "caption"; properties: FieldType["caption"] }
+  | { type: "download-link"; properties: FieldType["download-link"] }
+  | { type: "htmlIframe"; properties: FieldType["htmlIframe"] }
+  | { type: "array"; properties: FieldType["array"] }
+  | { type: "file"; properties: FieldType["file"] }
+  | { type: "amount"; properties: FieldType["amount"] }
+  | { type: "selectOne"; properties: FieldType["selectOne"] }
+  | {
+      type: "selectMultiple";
+      properties: FieldType["selectMultiple"];
+    }
+  | { type: "text"; properties: FieldType["text"] }
+  | { type: "textArea"; properties: FieldType["textArea"] }
+  | {
+      type: "choiceStacked";
+      properties: FieldType["choiceStacked"];
+    }
+  | {
+      type: "choiceHorizontal";
+      properties: FieldType["choiceHorizontal"];
+    }
+  | { type: "integer"; properties: FieldType["integer"] }
+  | { type: "toggle"; properties: FieldType["toggle"] }
+  | {
+      type: "absoluteTimeText";
+      properties: FieldType["absoluteTimeText"];
+    };
+
+export type FieldComponentFunction<key extends keyof FieldType> = (
+  props: FieldType[key],
+) => VNode;
+
+type UIFormFieldMap = {
+  [key in keyof FieldType]: FieldComponentFunction<key>;
+};
+
+/**
+ * Maps input type with component implementation
+ */
+export const UIFormConfiguration: UIFormFieldMap = {
+  group: Group,
+  "download-link": DownloadLink,
+  caption: Caption,
+  htmlIframe: HtmlIframe,
+  //@ts-ignore
+  array: InputArray,
+  text: InputText,
+  //@ts-ignore
+  file: InputFile,
+  textArea: InputTextArea,
+  //@ts-ignore
+  absoluteTimeText: InputAbsoluteTime,
+  //@ts-ignore
+  choiceStacked: InputChoiceStacked,
+  //@ts-ignore
+  choiceHorizontal: InputChoiceHorizontal,
+  integer: InputInteger,
+  //@ts-ignore
+  selectOne: InputSelectOne,
+  //@ts-ignore
+  selectMultiple: InputSelectMultiple,
+  //@ts-ignore
+  toggle: InputToggle,
+  //@ts-ignore
+  amount: InputAmount,
+};
diff --git a/packages/web-util/src/forms/ui-form.ts 
b/packages/web-util/src/forms/forms-types.ts
similarity index 92%
rename from packages/web-util/src/forms/ui-form.ts
rename to packages/web-util/src/forms/forms-types.ts
index 14f22cc1f..e604ce274 100644
--- a/packages/web-util/src/forms/ui-form.ts
+++ b/packages/web-util/src/forms/forms-types.ts
@@ -3,7 +3,6 @@ import {
   buildCodecForUnion,
   Codec,
   codecForBoolean,
-  codecForCanonBaseUrl,
   codecForConstString,
   codecForLazy,
   codecForList,
@@ -17,17 +16,22 @@ import {
   TalerProtocolTimestamp,
   TranslatedString,
 } from "@gnu-taler/taler-util";
-import { InternationalizationAPI } from "../index.browser.js";
 
-export type FormConfiguration = DoubleColumnForm | SingleColumnForm;
+export type FormDesign = DoubleColumnFormDesign | SingleColumnFormDesign;
 
-export type DoubleColumnForm = {
+/**
+ * form with composed by multiple sections
+ */
+export type DoubleColumnFormDesign = {
   type: "double-column";
-  design: DoubleColumnFormSection[];
+  sections: DoubleColumnFormSection[];
   // behavior?: (form: Partial<T>) => FormState<T>;
 };
 
-export type SingleColumnForm = {
+/**
+ * single section form
+ */
+export type SingleColumnFormDesign = {
   type: "single-column";
   fields: UIFormElementConfig[];
 };
@@ -38,11 +42,6 @@ export type DoubleColumnFormSection = {
   fields: UIFormElementConfig[];
 };
 
-// export interface BaseForm {
-//   state: TalerExchangeApi.AmlState;
-//   threshold: AmountJson;
-// }
-
 export type UIFormElementConfig =
   | UIFormElementGroup
   | UIFormElementCaption
@@ -373,24 +372,24 @@ const codecForDoubleColumnFormSection = (): 
Codec<DoubleColumnFormSection> =>
     .property("fields", codecForList(codecForUiFormField()))
     .build("DoubleColumnFormSection");
 
-const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
-  buildCodecForObject<DoubleColumnForm>()
+const codecForDoubleColumnFormDesign = (): Codec<DoubleColumnFormDesign> =>
+  buildCodecForObject<DoubleColumnFormDesign>()
     .property("type", codecForConstString("double-column"))
-    .property("design", codecForList(codecForDoubleColumnFormSection()))
-    .build("DoubleColumnForm");
+    .property("sections", codecForList(codecForDoubleColumnFormSection()))
+    .build("DoubleColumnFormDesign");
 
-const codecForSingleColumnForm = (): Codec<SingleColumnForm> =>
-  buildCodecForObject<SingleColumnForm>()
+const codecForSingleColumnFormDesign = (): Codec<SingleColumnFormDesign> =>
+  buildCodecForObject<SingleColumnFormDesign>()
     .property("type", codecForConstString("single-column"))
     .property("fields", codecForList(codecForUiFormField()))
-    .build("SingleColumnForm");
+    .build("SingleColumnFormDesign");
 
-const codecForFormConfiguration = (): Codec<FormConfiguration> =>
-  buildCodecForUnion<FormConfiguration>()
+const codecForFormDesign = (): Codec<FormDesign> =>
+  buildCodecForUnion<FormDesign>()
     .discriminateOn("type")
-    .alternative("double-column", codecForDoubleColumnForm())
-    .alternative("single-column", codecForSingleColumnForm())
-    .build<FormConfiguration>("FormConfiguration");
+    .alternative("double-column", codecForDoubleColumnFormDesign())
+    .alternative("single-column", codecForSingleColumnFormDesign())
+    .build<FormDesign>("FormDesign");
 
 const codecForFormMetadata = (): Codec<FormMetadata> =>
   buildCodecForObject<FormMetadata>()
@@ -398,7 +397,7 @@ const codecForFormMetadata = (): Codec<FormMetadata> =>
     .property("description", codecOptional(codecForString()))
     .property("id", codecForString())
     .property("version", codecForNumber())
-    .property("config", codecForFormConfiguration())
+    .property("config", codecForFormDesign())
     .build("FormMetadata");
 
 export const codecForUIForms = (): Codec<UiForms> =>
@@ -411,7 +410,7 @@ export type FormMetadata = {
   description?: string;
   id: string;
   version: number;
-  config: FormConfiguration;
+  config: FormDesign;
 };
 
 export interface UiForms {
diff --git a/packages/web-util/src/forms/forms-ui.tsx 
b/packages/web-util/src/forms/forms-ui.tsx
new file mode 100644
index 000000000..3f9dc4f5f
--- /dev/null
+++ b/packages/web-util/src/forms/forms-ui.tsx
@@ -0,0 +1,129 @@
+import { Fragment, h, h as create, VNode } from "preact";
+import { FormHandler, useForm } from "../hooks/useForm.js";
+// import { getConverterById, useTranslationContext } from 
"../index.browser.js";
+import { convertFormConfigToUiField } from "./forms-utils.js";
+import {
+  DoubleColumnFormSection,
+  FormDesign,
+  UIFormElementConfig,
+} from "./forms-types.js";
+import {
+  FieldComponentFunction,
+  UIFormConfiguration,
+  UIFormField,
+} from "./field-types.js";
+import { useTranslationContext } from "../index.browser.js";
+
+export function DefaultForm<T>({
+  design,
+  initial,
+}: {
+  design: FormDesign;
+  initial: object;
+}): VNode {
+  const { handler, status } = useForm(design, initial);
+
+  return (
+    <div>
+      <FormUI design={design} handler={handler} />
+      <pre class="break-all whitespace-pre-wrap">
+        {JSON.stringify({ status }, undefined, 2)}
+      </pre>
+    </div>
+  );
+}
+
+export function FormUI<T>({
+  design,
+  handler,
+}: {
+  design: FormDesign;
+  handler: FormHandler<T>;
+}): VNode {
+  switch (design.type) {
+    case "double-column": {
+      const ui = design.sections.map((section, i) => {
+        if (!section) return <Fragment />;
+        return (
+          <DoubleColumnFormSectionUI section={section} handler={handler} />
+        );
+      });
+      return <Fragment>{ui}</Fragment>;
+    }
+    case "single-column": {
+      return (
+        <SingleColumnFormSectionUI fields={design.fields} handler={handler} />
+      );
+    }
+  }
+}
+
+export function DoubleColumnFormSectionUI<T>({
+  section,
+  handler,
+}: {
+  handler: FormHandler<T>;
+  section: DoubleColumnFormSection;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+      <div class="px-4 sm:px-0">
+        <h2 class="text-base font-semibold leading-7 text-gray-900">
+          {section.title}
+        </h2>
+        {section.description && (
+          <p class="mt-1 text-sm leading-6 text-gray-600">
+            {section.description}
+          </p>
+        )}
+      </div>
+      <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
md:col-span-2">
+        <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, section.fields, 
handler)}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+export function SingleColumnFormSectionUI<T>({
+  fields,
+  handler,
+}: {
+  handler: FormHandler<T>;
+  fields: UIFormElementConfig[];
+}): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
md:col-span-2">
+      <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)}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export function RenderAllFieldsByUiConfig({
+  fields,
+}: {
+  fields: UIFormField[];
+}): VNode {
+  return create(
+    Fragment,
+    {},
+    fields.map((field, i) => {
+      const Component = UIFormConfiguration[
+        field.type
+      ] as FieldComponentFunction<any>;
+      return Component(field.properties);
+    }),
+  );
+}
diff --git a/packages/web-util/src/forms/forms-utils.ts 
b/packages/web-util/src/forms/forms-utils.ts
new file mode 100644
index 000000000..5322913cf
--- /dev/null
+++ b/packages/web-util/src/forms/forms-utils.ts
@@ -0,0 +1,382 @@
+import {
+  AbsoluteTime,
+  AmountJson,
+  Amounts,
+  assertUnreachable,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+  InternationalizationAPI,
+  UIFieldElementDescription,
+} from "../index.browser.js";
+import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
+import { UIFormElementConfig, UIFormFieldBaseConfig } from "./forms-types.js";
+import { UIFormField } from "./field-types.js";
+import { format, parse } from "date-fns";
+
+/**
+ * convert field configuration to render function
+ * FIXME: change this mapping for something not so insane
+ *
+ * @param i18n_
+ * @param fieldConfig
+ * @param form
+ * @returns
+ */
+export function convertFormConfigToUiField(
+  i18n_: InternationalizationAPI,
+  fieldConfig: UIFormElementConfig[],
+  form: object,
+): UIFormField[] {
+  return fieldConfig.map((config) => {
+    // NON input fields
+    switch (config.type) {
+      case "caption": {
+        const resp: UIFormField = {
+          type: config.type,
+          properties: converBaseFieldsProps(i18n_, config),
+        };
+        return resp;
+      }
+      case "download-link": {
+        const resp: UIFormField = {
+          type: config.type,
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            label: i18n_.str`${config.label}`,
+            url: config.url,
+            media: config.media,
+          },
+        };
+        return resp;
+      }
+      case "htmlIframe": {
+        const resp: UIFormField = {
+          type: config.type,
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            url: config.url,
+          },
+        };
+        return resp;
+      }
+      case "group": {
+        const resp: UIFormField = {
+          type: config.type,
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            fields: convertFormConfigToUiField(i18n_, config.fields, form),
+          },
+        };
+        return resp;
+      }
+    }
+    // Input Fields
+    switch (config.type) {
+      case "array": {
+        return {
+          type: "array",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            labelField: config.labelFieldId,
+            fields: config.fields,
+            // convertFormConfigToUiField(
+            //   i18n_,
+            //   config.fields,
+            //   (form as any)[config.id].value ?? {},
+            //   getConverterByFieldType,
+            // ),
+          },
+        } as UIFormField;
+      }
+      case "absoluteTimeText": {
+        return {
+          type: "absoluteTimeText",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
+      case "amount": {
+        return {
+          type: "amount",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            currency: config.currency,
+          },
+        } as UIFormField;
+      }
+      case "choiceHorizontal": {
+        return {
+          type: "choiceHorizontal",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            choices: config.choices,
+          },
+        } as UIFormField;
+      }
+      case "choiceStacked": {
+        return {
+          type: "choiceStacked",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            choices: config.choices,
+          },
+        } as UIFormField;
+      }
+      case "file": {
+        return {
+          type: "file",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            accept: config.accept,
+            maxBites: config.maxBytes,
+          },
+        } as UIFormField;
+      }
+      case "integer": {
+        return {
+          type: "integer",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
+      case "selectMultiple": {
+        return {
+          type: "selectMultiple",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            choices: config.choices,
+            unique: config.unique,
+          },
+        } as UIFormField;
+      }
+      case "selectOne": {
+        return {
+          type: "selectOne",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            choices: config.choices,
+          },
+        } as UIFormField;
+      }
+      case "text": {
+        return {
+          type: "text",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
+      case "textArea": {
+        return {
+          type: "textArea",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
+      case "toggle": {
+        return {
+          type: "toggle",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+            threeState: config.threeState,
+          },
+        } as UIFormField;
+      }
+      default: {
+        assertUnreachable(config);
+      }
+    }
+  });
+}
+
+function getAddonById(_id: string | undefined): Addon {
+  return undefined!;
+}
+
+function getConverterByFieldType(
+  fieldType: string | undefined,
+  config: unknown,
+): StringConverter<unknown> {
+  if (fieldType === "absoluteTimeText") {
+    // @ts-expect-error check this
+    return absTimeConverter(config);
+  }
+  if (fieldType === "amount") {
+    // @ts-expect-error check this
+    return amountConverter(config);
+  }
+  if (fieldType === "TalerExchangeApi.AmlState") {
+    // @ts-expect-error check this
+    return amlStateConverter;
+  }
+  return nullConverter as StringConverter<unknown>;
+}
+
+function converInputFieldsProps(
+  form: object,
+  p: UIFormFieldBaseConfig,
+  converter: StringConverter<unknown>,
+) {
+  const names = p.id.split(".");
+  // console.log("NAMES", names, getValueDeeper2(form, names), form)
+  return {
+    converter,
+    handler: getValueDeeper2(form, names),
+    required: p.required,
+    disabled: p.disabled,
+    name: names[names.length - 1],
+    help: p.help,
+    placeholder: p.placeholder,
+    tooltip: p.tooltip,
+    label: p.label as TranslatedString,
+  };
+}
+
+function converBaseFieldsProps(
+  i18n_: InternationalizationAPI,
+  p: UIFieldElementDescription,
+) {
+  return {
+    after: getAddonById(p.addonAfterId),
+    before: getAddonById(p.addonBeforeId),
+    hidden: p.hidden,
+    help: i18n_.str`${p.help}`,
+    label: i18n_.str`${p.label}`,
+    tooltip: i18n_.str`${p.tooltip}`,
+  };
+}
+
+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 ?? "";
+  },
+  toStringUI(v: unknown): string {
+    return v as string;
+  },
+};
+
+function amountConverter(config: any): StringConverter<AmountJson> {
+  const currency = config["currency"];
+  if (!currency || typeof currency !== "string") {
+    throw Error(`amount converter needs a currency`);
+  }
+  return {
+    fromStringUI(v: string | undefined): AmountJson {
+      return (
+        Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency)
+      );
+    },
+    toStringUI(v: unknown): string {
+      return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+    },
+  };
+}
+
+function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
+  const pattern = config["pattern"];
+  if (!pattern || typeof pattern !== "string") {
+    throw Error(`absTime converter needs a pattern`);
+  }
+  return {
+    fromStringUI(v: string | undefined): AbsoluteTime {
+      if (v === undefined) {
+        return AbsoluteTime.never();
+      }
+      try {
+        const time = parse(v, pattern, new Date());
+        return AbsoluteTime.fromMilliseconds(time.getTime());
+      } catch (e) {
+        return AbsoluteTime.never();
+      }
+    },
+    toStringUI(v: unknown): string {
+      if (v === undefined) return "";
+      const d = v as AbsoluteTime;
+      if (d.t_ms === "never") return "never";
+      try {
+        return format(d.t_ms, pattern);
+      } catch (e) {
+        return "";
+      }
+    },
+  };
+}
diff --git a/packages/web-util/src/forms/forms.ts 
b/packages/web-util/src/forms/forms.ts
deleted file mode 100644
index 178a3b626..000000000
--- a/packages/web-util/src/forms/forms.ts
+++ /dev/null
@@ -1,411 +0,0 @@
-import { h as create, Fragment, VNode } from "preact";
-import { Caption } from "./Caption.js";
-import { Group } from "./Group.js";
-import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
-import { InputAmount } from "./InputAmount.js";
-import { InputArray } from "./InputArray.js";
-import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
-import { InputChoiceStacked } from "./InputChoiceStacked.js";
-import { InputFile } from "./InputFile.js";
-import { InputInteger } from "./InputInteger.js";
-import { InputSelectMultiple } from "./InputSelectMultiple.js";
-import { InputSelectOne } from "./InputSelectOne.js";
-import { InputText } from "./InputText.js";
-import { InputTextArea } from "./InputTextArea.js";
-import { InputToggle } from "./InputToggle.js";
-import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
-import {
-  InternationalizationAPI,
-  UIFieldElementDescription,
-} from "../index.browser.js";
-import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
-import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js";
-import { HtmlIframe } from "./HtmlIframe.js";
-import { DownloadLink } from "./DownloadLink.js";
-/**
- * Constrain the type with the ui props
- */
-type FieldType<T extends object = any, K extends keyof T = any> = {
-  group: Parameters<typeof Group>[0];
-  caption: Parameters<typeof Caption>[0];
-  "download-link": Parameters<typeof DownloadLink>[0];
-  htmlIframe: Parameters<typeof HtmlIframe>[0];
-  array: Parameters<typeof InputArray<T, K>>[0];
-  file: Parameters<typeof InputFile<T, K>>[0];
-  selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
-  selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
-  text: Parameters<typeof InputText<T, K>>[0];
-  textArea: Parameters<typeof InputTextArea<T, K>>[0];
-  choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
-  choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
-  absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
-  integer: Parameters<typeof InputInteger<T, K>>[0];
-  toggle: Parameters<typeof InputToggle<T, K>>[0];
-  amount: Parameters<typeof InputAmount<T, K>>[0];
-};
-
-/**
- * List all the form fields so typescript can type-check the form instance
- */
-export type UIFormField =
-  | { type: "group"; properties: FieldType["group"] }
-  | { type: "caption"; properties: FieldType["caption"] }
-  | { type: "download-link"; properties: FieldType["download-link"] }
-  | { type: "htmlIframe"; properties: FieldType["htmlIframe"] }
-  | { type: "array"; properties: FieldType["array"] }
-  | { type: "file"; properties: FieldType["file"] }
-  | { type: "amount"; properties: FieldType["amount"] }
-  | { type: "selectOne"; properties: FieldType["selectOne"] }
-  | {
-      type: "selectMultiple";
-      properties: FieldType["selectMultiple"];
-    }
-  | { type: "text"; properties: FieldType["text"] }
-  | { type: "textArea"; properties: FieldType["textArea"] }
-  | {
-      type: "choiceStacked";
-      properties: FieldType["choiceStacked"];
-    }
-  | {
-      type: "choiceHorizontal";
-      properties: FieldType["choiceHorizontal"];
-    }
-  | { type: "integer"; properties: FieldType["integer"] }
-  | { type: "toggle"; properties: FieldType["toggle"] }
-  | {
-      type: "absoluteTimeText";
-      properties: FieldType["absoluteTimeText"];
-    };
-
-type FieldComponentFunction<key extends keyof FieldType> = (
-  props: FieldType[key],
-) => VNode;
-
-type UIFormFieldMap = {
-  [key in keyof FieldType]: FieldComponentFunction<key>;
-};
-
-/**
- * Maps input type with component implementation
- */
-const UIFormConfiguration: UIFormFieldMap = {
-  group: Group,
-  "download-link": DownloadLink,
-  caption: Caption,
-  htmlIframe: HtmlIframe,
-  //@ts-ignore
-  array: InputArray,
-  text: InputText,
-  //@ts-ignore
-  file: InputFile,
-  textArea: InputTextArea,
-  //@ts-ignore
-  absoluteTimeText: InputAbsoluteTime,
-  //@ts-ignore
-  choiceStacked: InputChoiceStacked,
-  //@ts-ignore
-  choiceHorizontal: InputChoiceHorizontal,
-  integer: InputInteger,
-  //@ts-ignore
-  selectOne: InputSelectOne,
-  //@ts-ignore
-  selectMultiple: InputSelectMultiple,
-  //@ts-ignore
-  toggle: InputToggle,
-  //@ts-ignore
-  amount: InputAmount,
-};
-
-export function RenderAllFieldsByUiConfig({
-  fields,
-}: {
-  fields: UIFormField[];
-}): VNode {
-  return create(
-    Fragment,
-    {},
-    fields.map((field, i) => {
-      const Component = UIFormConfiguration[
-        field.type
-      ] as FieldComponentFunction<any>;
-      return Component(field.properties);
-    }),
-  );
-}
-
-// type FormSet<T extends object> = {
-//   Provider: typeof FormProvider<T>;
-//   InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
-//   InputChoiceHorizontal: <K extends keyof T>() => typeof 
InputChoiceHorizontal<T, K>;
-// };
-
-/**
- * Helper function that created a typed object.
- *
- * @returns
- */
-// export function createNewForm<T extends object>() {
-//   const res: FormSet<T> = {
-//     Provider: FormProvider,
-//     InputLine: () => InputLine,
-//     InputChoiceHorizontal: () => InputChoiceHorizontal,
-//   };
-//   return {
-//     Provider: res.Provider,
-//     InputLine: res.InputLine(),
-//     InputChoiceHorizontal: res.InputChoiceHorizontal(),
-//   };
-// }
-
-/**
- * convert field configuration to render function
- *
- * @param i18n_
- * @param fieldConfig
- * @param form
- * @returns
- */
-export function convertUiField(
-  i18n_: InternationalizationAPI,
-  fieldConfig: UIFormElementConfig[],
-  form: object,
-  getConverterById: GetConverterById,
-): UIFormField[] {
-  return fieldConfig.map((config) => {
-    // NON input fields
-    switch (config.type) {
-      case "caption": {
-        const resp: UIFormField = {
-          type: config.type,
-          properties: converBaseFieldsProps(i18n_, config),
-        };
-        return resp;
-      }
-      case "download-link": {
-        const resp: UIFormField = {
-          type: config.type,
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            label: i18n_.str`${config.label}`,
-            url: config.url,
-            media: config.media,
-          },
-        };
-        return resp;
-      }
-      case "htmlIframe": {
-        const resp: UIFormField = {
-          type: config.type,
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            url: config.url,
-          },
-        };
-        return resp;
-      }
-      case "group": {
-        const resp: UIFormField = {
-          type: config.type,
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            fields: convertUiField(
-              i18n_,
-              config.fields,
-              form,
-              getConverterById,
-            ),
-          },
-        };
-        return resp;
-      }
-    }
-    // Input Fields
-    switch (config.type) {
-      case "array": {
-        return {
-          type: "array",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            labelField: config.labelFieldId,
-            fields: config.fields,
-            // convertUiField(
-            //   i18n_,
-            //   config.fields,
-            //   (form as any)[config.id].value ?? {},
-            //   getConverterById,
-            // ),
-          },
-        } as UIFormField;
-      }
-      case "absoluteTimeText": {
-        return {
-          type: "absoluteTimeText",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-          },
-        } as UIFormField;
-      }
-      case "amount": {
-        return {
-          type: "amount",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            currency: config.currency,
-          },
-        } as UIFormField;
-      }
-      case "choiceHorizontal": {
-        return {
-          type: "choiceHorizontal",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            choices: config.choices,
-          },
-        } as UIFormField;
-      }
-      case "choiceStacked": {
-        return {
-          type: "choiceStacked",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            choices: config.choices,
-          },
-        } as UIFormField;
-      }
-      case "file": {
-        return {
-          type: "file",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            accept: config.accept,
-            maxBites: config.maxBytes,
-          },
-        } as UIFormField;
-      }
-      case "integer": {
-        return {
-          type: "integer",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-          },
-        } as UIFormField;
-      }
-      case "selectMultiple": {
-        return {
-          type: "selectMultiple",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            choices: config.choices,
-          },
-        } as UIFormField;
-      }
-      case "selectOne": {
-        return {
-          type: "selectOne",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            choices: config.choices,
-          },
-        } as UIFormField;
-      }
-      case "text": {
-        return {
-          type: "text",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-          },
-        } as UIFormField;
-      }
-      case "textArea": {
-        return {
-          type: "textArea",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-          },
-        } as UIFormField;
-      }
-      case "toggle": {
-        return {
-          type: "toggle",
-          properties: {
-            ...converBaseFieldsProps(i18n_, config),
-            ...converInputFieldsProps(form, config, getConverterById),
-            threeState: config.threeState,
-          },
-        } as UIFormField;
-      }
-      default: {
-        assertUnreachable(config);
-      }
-    }
-  });
-}
-
-function getAddonById(_id: string | undefined): Addon {
-  return undefined!;
-}
-
-type GetConverterById = (
-  id: string | undefined,
-  config: unknown,
-) => StringConverter<unknown>;
-
-function converInputFieldsProps(
-  form: object,
-  p: UIFormFieldBaseConfig,
-  getConverterById: GetConverterById,
-) {
-  const names = p.id.split(".");
-  // console.log("NAMES", names, getValueDeeper2(form, names), form)
-  return {
-    converter: getConverterById(p.converterId, p),
-    handler: getValueDeeper2(form, names),
-    required: p.required,
-    disabled: p.disabled,
-    name: names[names.length - 1],
-    help: p.help,
-    placeholder: p.placeholder,
-    tooltip: p.tooltip,
-    label: p.label as TranslatedString,
-  };
-}
-
-function converBaseFieldsProps(
-  i18n_: InternationalizationAPI,
-  p: UIFieldElementDescription,
-) {
-  return {
-    after: getAddonById(p.addonAfterId),
-    before: getAddonById(p.addonBeforeId),
-    hidden: p.hidden,
-    help: i18n_.str`${p.help}`,
-    label: i18n_.str`${p.label}`,
-    tooltip: i18n_.str`${p.tooltip}`,
-  };
-}
-
-export 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);
-}
diff --git a/packages/web-util/src/forms/index.stories.ts 
b/packages/web-util/src/forms/index.stories.ts
index 55878cb02..b08c727a4 100644
--- a/packages/web-util/src/forms/index.stories.ts
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -5,7 +5,6 @@ export * as a4 from "./InputChoiceStacked.stories.js";
 export * as a5 from "./InputAbsoluteTime.stories.js";
 export * as a6 from "./InputFile.stories.js";
 export * as a7 from "./InputInteger.stories.js";
-export * as a8 from "./InputLine.stories.js";
 export * as a9 from "./InputSelectMultiple.stories.js";
 export * as a10 from "./InputSelectOne.stories.js";
 export * as a11 from "./InputText.stories.js";
diff --git a/packages/web-util/src/forms/index.ts 
b/packages/web-util/src/forms/index.ts
index 5e3f4e1d3..187fffef3 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -1,26 +1,23 @@
-export * from "./Calendar.js"
-export * from "./Caption.js"
-export * from "./HtmlIframe.js"
-export * from "./DefaultForm.js"
-export * from "./Dialog.js"
-export * from "./FormProvider.js"
-export * from "./Group.js"
-export * from "./InputAbsoluteTime.js"
-export * from "./InputAmount.js"
-export * from "./InputArray.js"
-export * from "./InputChoiceHorizontal.js"
-export * from "./InputChoiceStacked.js"
-export * from "./InputFile.js"
-export * from "./InputInteger.js"
-export * from "./InputLine.js"
-export * from "./InputSelectMultiple.js"
-export * from "./InputSelectOne.js"
-export * from "./InputText.js"
-export * from "./InputTextArea.js"
-export * from "./InputToggle.js"
-export * from "./TimePicker.js"
-export * from "./forms.js"
-export * from "./ui-form.js"
-export * from "./converter.js"
-export * from "./useField.js"
-
+export * from "./Calendar.js";
+export * from "./Caption.js";
+export * from "./HtmlIframe.js";
+export * from "./Dialog.js";
+export * from "./FormProvider.js";
+export * from "./Group.js";
+export * from "./InputAbsoluteTime.js";
+export * from "./InputAmount.js";
+export * from "./InputArray.js";
+export * from "./InputChoiceHorizontal.js";
+export * from "./InputChoiceStacked.js";
+export * from "./InputFile.js";
+export * from "./InputInteger.js";
+export * from "./InputLine.js";
+export * from "./InputSelectMultiple.js";
+export * from "./InputSelectOne.js";
+export * from "./InputText.js";
+export * from "./InputTextArea.js";
+export * from "./InputToggle.js";
+export * from "./TimePicker.js";
+export * from "./field-types.js";
+export * from "./forms-types.js";
+export * from "./forms-ui.js";
diff --git a/packages/web-util/src/forms/useField.ts 
b/packages/web-util/src/forms/useField.ts
index a250d3100..1b4bce02a 100644
--- a/packages/web-util/src/forms/useField.ts
+++ b/packages/web-util/src/forms/useField.ts
@@ -11,10 +11,10 @@ export interface InputFieldHandler<Type> {
 
 /**
  * @deprecated removing this so we don't depend on context to create a form
- * @param name 
- * @returns 
+ * @param name
+ * @returns
  */
-export function useField<T extends object, K extends keyof T>(
+export function useField_deprecated<T extends object, K extends keyof T>(
   name: K,
 ): InputFieldHandler<T[K]> | undefined {
   const ctx = useContext(FormContext);
@@ -27,7 +27,7 @@ export function useField<T extends object, K extends keyof T>(
     computeFormState,
     onUpdate: notifyUpdate,
     readOnly: readOnlyForm,
-  } = ctx
+  } = ctx;
 
   type P = typeof name;
   type V = T[P];
@@ -40,7 +40,7 @@ export function useField<T extends object, K extends keyof T>(
 
   //compute default state
   const state = {
-    disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
+    disabled: readOnlyForm ? true : fieldState.disabled ?? false,
     hidden: fieldState.hidden ?? false,
     help: fieldState.help,
     elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
@@ -48,7 +48,7 @@ export function useField<T extends object, K extends keyof T>(
 
   function onChange(value: V): void {
     // setCurrentValue(value);
-    formValue.current = setValueDeeper(
+    formValue.current = setValueDeeper_toberemoved(
       formValue.current,
       String(name).split("."),
       value,
@@ -72,20 +72,24 @@ export function useField<T extends object, K extends keyof 
T>(
  * @param name
  * @returns
  */
-function readField<T>(
-  object: any,
-  name: string,
-): T | undefined {
+function readField<T>(object: any, name: string): T | undefined {
   return name.split(".").reduce((prev, current) => {
     return prev ? prev[current] : undefined;
   }, object);
 }
 
-function setValueDeeper(object: any, names: string[], value: any): any {
+function setValueDeeper_toberemoved(
+  object: any,
+  names: string[],
+  value: any,
+): any {
   if (names.length === 0) return value;
   const [head, ...rest] = names;
   if (object === undefined) {
-    return { [head]: setValueDeeper({}, rest, value) };
+    return { [head]: setValueDeeper_toberemoved({}, rest, value) };
   }
-  return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) 
};
+  return {
+    ...object,
+    [head]: setValueDeeper_toberemoved(object[head] ?? {}, rest, value),
+  };
 }
diff --git a/packages/web-util/src/hooks/index.ts 
b/packages/web-util/src/hooks/index.ts
index ba1b6e222..35c1e3186 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,7 +1,13 @@
 export { useLang } from "./useLang.js";
-export { useLocalStorage, buildStorageKey, StorageKey, StorageState } from 
"./useLocalStorage.js";
+export {
+  useLocalStorage,
+  buildStorageKey,
+  StorageKey,
+  StorageState,
+} from "./useLocalStorage.js";
 export { useMemoryStorage } from "./useMemoryStorage.js";
 export * from "./useNotifications.js";
+export { useForm } from "./useForm.js";
 export {
   useAsyncAsHook,
   HookError,
diff --git a/packages/web-util/src/hooks/useForm.ts 
b/packages/web-util/src/hooks/useForm.ts
index a8ce98d11..a669f3007 100644
--- a/packages/web-util/src/hooks/useForm.ts
+++ b/packages/web-util/src/hooks/useForm.ts
@@ -17,10 +17,12 @@
 import {
   AbsoluteTime,
   AmountJson,
+  assertUnreachable,
   TalerExchangeApi,
   TranslatedString,
 } from "@gnu-taler/taler-util";
 import {
+  FormDesign,
   UIFieldHandler,
   UIFormElementConfig,
   UIHandlerId,
@@ -75,139 +77,113 @@ export type FormStatus<T> =
       errors: FormErrors<T>;
     };
 
-function constructFormHandler<T>(
-  shape: Array<UIHandlerId>,
-  form: RecursivePartial<FormValues<T>>,
-  updateForm: (d: RecursivePartial<FormValues<T>>) => void,
-  errors: FormErrors<T> | undefined,
-): FormHandler<T> {
-  const handler = shape.reduce((handleForm, fieldId) => {
-    const path = fieldId.split(".");
+export type FormState<T> = {
+  handler: FormHandler<T>;
+  status: FormStatus<T>;
+};
 
-    function updater(newValue: unknown) {
-      updateForm(setValueDeeper(form, path, newValue));
-    }
+function checkAllRequirements<T>(
+  st: RecursivePartial<FormValues<T>>,
+  check: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
+): FormStatus<T> {
+  const errors = undefinedIfEmpty<FormErrors<T> | undefined>(
+    // validateRequiredFields(st, config),
+    check(st),
+  );
 
-    const currentValue = getValueDeeper<string>(form as any, path, undefined);
-    const currentError = getValueDeeper<TranslatedString>(
-      errors as any,
-      path,
-      undefined,
-    );
-    const field: UIFieldHandler = {
-      error: currentError,
-      value: currentValue,
-      onChange: updater,
-      state: {}, //FIXME: add the state of the field (hidden, )
+  if (errors !== undefined) {
+    return {
+      status: "fail" as const,
+      result: st as any,
+      errors,
     };
+  }
 
-    return setValueDeeper(handleForm, path, field);
-  }, {} as FormHandler<T>);
-
-  return handler;
+  return { status: "ok" as const, result: st as any, errors: undefined };
 }
 
-export function useFormStateFromConfig<T>(
-  fields: Array<UIFormElementConfig>,
-  defaultValue: RecursivePartial<FormValues<T>>,
-  check?: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
-): [FormHandler<T>, FormStatus<T>] {
-  const shape: Array<UIHandlerId> = [];
-  const requiredFields: Array<UIHandlerId> = [];
-  Array.prototype.push.apply(shape, getShapeFromFields(fields));
-  Array.prototype.push.apply(requiredFields, getRequiredFields(fields));
-
-  const [form, updateForm] =
-    useState<RecursivePartial<FormValues<T>>>(defaultValue);
-
-  function defaultCheckAllRequired(st: RecursivePartial<FormValues<T>>) {
-    const partialErrors = undefinedIfEmpty<FormErrors<T>>({});
-
-    const errors = undefinedIfEmpty<FormErrors<T> | undefined>(
-      validateRequiredFields(partialErrors, st, requiredFields),
-    );
+/**
+ *
+ * @param fields form fields
+ * @param initialValue initial value
+ * @param check validation chain
+ * @returns
+ */
+export function useForm<T>(
+  design: FormDesign,
+  initialValue: RecursivePartial<FormValues<T>>,
+  check?: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
+): FormState<T> {
+  const [formValue, formUpdateHandler] =
+    useState<RecursivePartial<FormValues<T>>>(initialValue);
 
-    if (errors !== undefined) {
-      return {
-        status: "fail" as const,
-        result: st as any,
-        errors,
-      };
+  const status = checkAllRequirements<T>(formValue, (v) => {
+    // FIXME: checks should be by fields
+    // FIXME: iterate only once and satify all checks, here we are potentially 
iterating more than once
+    const required = validateRequiredFields(v, design);
+    if (!required && check) {
+      return check(v);
     }
+    return required;
+  });
 
-    return undefined;
-  }
-  // check required fields
-  const requiredCheckResult =
-    requiredFields.length > 0 ? defaultCheckAllRequired(form) : undefined;
-  // verify if there is a custom check function and all required fields are ok
-  // if there no custom check return "ok"
-  const status =
-    requiredCheckResult ??
-    (check
-      ? check(form)
-      : { status: "ok" as const, result: form as any, errors: undefined });
   const handler = constructFormHandler(
-    shape,
-    form,
-    updateForm,
-    requiredCheckResult?.errors,
+    design,
+    formValue,
+    formUpdateHandler,
+    status?.errors,
   );
 
-  return [handler, status];
+  return { handler, status };
 }
 
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
 /**
- * @deprecated use `useFormStateFromConfig`
+ * Use $path to get the value of $object
+ * return $noFoundValue if the target property is undefined
  *
- * @param defaultValue
- * @param check
+ * @param object
+ * @param path
+ * @param notFoundValue
  * @returns
  */
-export function useFormState<T>(
-  shape: Array<UIHandlerId>,
-  defaultValue: RecursivePartial<FormValues<T>>,
-  check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
-): [FormHandler<T>, FormStatus<T>] {
-  const [form, updateForm] =
-    useState<RecursivePartial<FormValues<T>>>(defaultValue);
-
-  const status = check(form);
-  const handler = constructFormHandler(shape, form, updateForm, status.errors);
-
-  return [handler, status];
-}
-
-interface Tree<T> extends Record<string, Tree<T> | T> {}
-
-export function getValueDeeper<T>(
+export function getValueFromPath<T>(
   object: Tree<T> | undefined,
-  names: string[],
+  path: string[],
   notFoundValue?: T,
 ): T | undefined {
-  if (names.length === 0) return object as T;
-  const [head, ...rest] = names;
+  if (path.length === 0) return object as T;
+  const [head, ...rest] = path;
   if (!head) {
-    return getValueDeeper(object, rest, notFoundValue);
+    return getValueFromPath(object, rest, notFoundValue);
   }
   if (object === undefined) {
     return notFoundValue;
   }
-  return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+  return getValueFromPath(object[head] as Tree<T>, rest, notFoundValue);
 }
 
-export function setValueDeeper(object: any, names: string[], value: any): any {
-  if (names.length === 0) return value;
-  const [head, ...rest] = names;
+/**
+ * Use $path to set the value $value into $object
+ * Don't modify $object, returns a new value
+ * @param object
+ * @param path
+ * @param value
+ * @returns
+ */
+function setValueIntoPath(object: any, path: string[], value: any): any {
+  if (path.length === 0) return value;
+  const [head, ...rest] = path;
   if (!head) {
-    return setValueDeeper(object, rest, value);
+    return setValueIntoPath(object, rest, value);
   }
   if (object === undefined) {
-    return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+    return undefinedIfEmpty({ [head]: setValueIntoPath({}, rest, value) });
   }
   return undefinedIfEmpty({
     ...object,
-    [head]: setValueDeeper(object[head] ?? {}, rest, value),
+    [head]: setValueIntoPath(object[head] ?? {}, rest, value),
   });
 }
 
@@ -222,60 +198,105 @@ export function undefinedIfEmpty<T extends object | 
undefined>(
     : undefined;
 }
 
-export function getShapeFromFields(
-  fields: UIFormElementConfig[],
-): Array<UIHandlerId> {
-  const shape: Array<UIHandlerId> = [];
-  fields.forEach((field) => {
-    if ("id" in field) {
-      // FIXME: this should be a validation when loading the form
-      // consistency check
-      // if (shape.indexOf(field.id) !== -1) {
-      //   throw Error(`already present: ${field.id}`);
-      // }
-      shape.push(field.id);
-    } else if (field.type === "group") {
-      Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
-    }
-  });
-  return shape;
-}
-
-export function getRequiredFields(
-  fields: UIFormElementConfig[],
-): Array<UIHandlerId> {
-  const shape: Array<UIHandlerId> = [];
-  fields.forEach((field) => {
-    if ("id" in field) {
-      // FIXME: this should be a validation when loading the form
-      // consistency check
-      // if (shape.indexOf(field.id) !== -1) {
-      //   throw Error(`already present: ${field.id}`);
-      // }
-      if (!field.required) {
-        return;
-      }
-      shape.push(field.id);
-    } else if (field.type === "group") {
-      Array.prototype.push.apply(shape, getRequiredFields(field.fields));
-    }
-  });
-  return shape;
-}
 export function validateRequiredFields<FormType>(
-  errors: FormErrors<FormType> | undefined,
   form: object,
-  fields: Array<UIHandlerId>,
+  config: FormDesign,
 ): FormErrors<FormType> | undefined {
-  let result: FormErrors<FormType> | undefined = errors;
-  fields.forEach((f) => {
-    const path = f.split(".");
-    const v = getValueDeeper(form as any, path);
-    result = setValueDeeper(
+  let result: FormErrors<FormType> | undefined = undefined;
+
+  function checkIfRequiredFieldHasValue(formElement: UIFormElementConfig) {
+    if ("fields" in formElement) {
+      formElement.fields.forEach(checkIfRequiredFieldHasValue);
+    }
+    if (!("id" in formElement)) {
+      return;
+    }
+    if (!formElement.required) return;
+    const path = formElement.id.split(".");
+    const v = getValueFromPath(form as any, path);
+    result = setValueIntoPath(
       result,
       path,
       v === undefined ? "required" : undefined,
     );
-  });
+  }
+
+  switch (config.type) {
+    case "double-column": {
+      config.sections.forEach((sec) => {
+        sec.fields.forEach(checkIfRequiredFieldHasValue);
+      });
+      break;
+    }
+    case "single-column": {
+      config.fields.forEach(checkIfRequiredFieldHasValue);
+      break;
+    }
+    default: {
+      assertUnreachable(config);
+    }
+  }
+
   return result;
 }
+
+function constructFormHandler<T>(
+  design: FormDesign,
+  value: RecursivePartial<FormValues<T>>,
+  onValueChange: (d: RecursivePartial<FormValues<T>>) => void,
+  errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+  let formHandler: FormHandler<T> = {};
+
+  function notifyUpdateOnFieldChange(formElement: UIFormElementConfig): void {
+    if ("fields" in formElement) {
+      formElement.fields.forEach(notifyUpdateOnFieldChange);
+    }
+    if (!("id" in formElement)) {
+      return;
+    }
+    const path = formElement.id.split(".");
+
+    function updater(newValue: unknown) {
+      const updated = setValueIntoPath(value, path, newValue);
+      onValueChange(updated);
+    }
+
+    const currentValue = getValueFromPath<string>(
+      value as any,
+      path,
+      undefined,
+    );
+    const currentError = getValueFromPath<TranslatedString>(
+      errors as any,
+      path,
+      undefined,
+    );
+    const field: UIFieldHandler = {
+      error: currentError,
+      value: currentValue,
+      onChange: updater,
+      state: {}, //FIXME: add the state of the field (hidden, )
+    };
+
+    formHandler = setValueIntoPath(formHandler, path, field);
+  }
+
+  switch (design.type) {
+    case "double-column": {
+      design.sections.forEach((sec) => {
+        sec.fields.forEach(notifyUpdateOnFieldChange);
+      });
+      break;
+    }
+    case "single-column": {
+      design.fields.forEach(notifyUpdateOnFieldChange);
+      break;
+    }
+    default: {
+      assertUnreachable(design);
+    }
+  }
+
+  return formHandler;
+}
diff --git a/packages/web-util/src/stories.html 
b/packages/web-util/src/stories.html
index b4a36fc19..1ce5e5c1e 100644
--- a/packages/web-util/src/stories.html
+++ b/packages/web-util/src/stories.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html>
   <head>
     <title>WebUtils: Stories</title>
@@ -13,6 +13,10 @@
       href="__EXAMPLES_CSS_FILE_LOCATION__"
     />
     <script type="module" src="__EXAMPLES_JS_FILE_LOCATION__"></script>
+    <!-- FIXME: remove this -->
+    <!-- this is an easy setup of tailwind to test out the form fields -->
+    <!-- the css must be build locally and prevent the requirement of internet 
access for development -->
+    <script 
src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries";></script>
   </head>
   <body>
     <taler-stories id="container"></taler-stories>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]