summaryrefslogtreecommitdiff
path: root/packages/web-util/src/forms/FormProvider.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src/forms/FormProvider.tsx')
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx150
1 files changed, 150 insertions, 0 deletions
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
new file mode 100644
index 000000000..5e08efb32
--- /dev/null
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -0,0 +1,150 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import { MutableRef, useState } from "preact/hooks";
+
+export interface FormType<T extends object> {
+ value: MutableRef<Partial<T>>;
+ initial?: Partial<T>;
+ readOnly?: boolean;
+ onUpdate?: (v: Partial<T>) => void;
+ computeFormState?: (v: Partial<T>) => FormState<T>;
+}
+
+export const FormContext = createContext<FormType<any>| undefined>(undefined);
+
+/**
+ * Map of {[field]:FieldUIOptions}
+ * for every field of type
+ * - any native (string, number, etc...)
+ * - absoluteTime
+ * - amountJson
+ *
+ * except for:
+ * - object => recurse into
+ * - array => behavior result and element field
+ */
+export type FormState<T extends object | undefined> = {
+ [field in keyof T]?: T[field] extends AbsoluteTime
+ ? FieldUIOptions
+ : T[field] extends AmountJson
+ ? FieldUIOptions
+ : T[field] extends Array<infer P extends object>
+ ? InputArrayFieldState<P>
+ : T[field] extends object | undefined
+ ? FormState<T[field]>
+ : FieldUIOptions;
+};
+
+/**
+ * Properties that can be defined by design or by computing state
+ */
+export type FieldUIOptions = {
+ /* instruction to be shown in the field */
+ placeholder?: TranslatedString;
+ /* long text help to be shown on demand */
+ tooltip?: TranslatedString;
+ /* short text to be shown next to the field*/
+
+ help?: TranslatedString;
+ /* should show as disabled and readonly */
+ disabled?: boolean;
+ /* should not show */
+ hidden?: boolean;
+
+ /* show a mark as required*/
+ required?: boolean;
+};
+
+/**
+ * properties only to be defined on design time
+ */
+export interface UIFormProps<T extends object, K extends keyof T>
+ extends FieldUIOptions {
+ // property name of the object
+ name: K;
+
+ // label if the field
+ label: TranslatedString;
+ before?: Addon;
+ after?: Addon;
+
+ // converter to string and back
+ converter?: StringConverter<T[K]>;
+
+ handler?: UIFieldHandler;
+}
+
+export type UIFieldHandler = {
+ value: string | undefined;
+ onChange: (s: string) => void;
+ state: FieldUIOptions;
+ error?: TranslatedString;
+};
+
+export interface IconAddon {
+ type: "icon";
+ icon: VNode;
+}
+export interface ButtonAddon {
+ type: "button";
+ onClick: () => void;
+ children: ComponentChildren;
+}
+export interface TextAddon {
+ type: "text";
+ text: TranslatedString;
+}
+export type Addon = IconAddon | ButtonAddon | TextAddon;
+
+export interface StringConverter<T> {
+ toStringUI: (v?: T) => string;
+ fromStringUI: (v?: string) => T;
+}
+
+export interface InputArrayFieldState<P extends object> extends FieldUIOptions {
+ elements?: FormState<P>[];
+}
+
+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>
+ );
+}