diff options
Diffstat (limited to 'packages/web-util/src/forms/FormProvider.tsx')
-rw-r--r-- | packages/web-util/src/forms/FormProvider.tsx | 150 |
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> + ); +} |