summaryrefslogtreecommitdiff
path: root/packages/web-util/src/forms/FormProvider.tsx
blob: f4616525ba24e828429e361817611825023b780d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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>;
}

//@ts-ignore
export const FormContext = createContext<FormType<any>>({});

/**
 * 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 = {
  /* text to be shown next to the field */
  error?: TranslatedString;
  /* 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]>;
}

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>
  );
}