[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.