aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
blob: b9f9f78328d0d686b2814b3524a7c44418abf9f6 (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
import {
  AbsoluteTime,
  AmountJson,
  TranslatedString,
} from "@gnu-taler/taler-util";
import { ComponentChildren, VNode, createContext, h } from "preact";
import {
  MutableRef,
  StateUpdater,
  useState
} from "preact/hooks";

export interface FormType<T extends object> {
  value: MutableRef<Partial<T>>;
  initialValue?: Partial<T>;
  readOnly?: boolean;
  onUpdate?: StateUpdater<T>;
  computeFormState?: (v: T) => FormState<T>;
}

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

/**
 * Map of {[field]:BehaviorResult}
 * 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
  ? BehaviorResult
  : T[field] extends AmountJson
  ? BehaviorResult
  : T[field] extends Array<infer P extends object>
  ? InputArrayFieldState<P>
  : T[field] extends (object | undefined)
  ? FormState<T[field]>
  : BehaviorResult;
};

export type BehaviorResult = Partial<InputFieldState> & FieldUIOptions

export interface InputFieldState {
  /* should show the error */
  error?: TranslatedString;
  /* should not allow to edit */
  readonly: boolean;
  /* should show as disable */
  disabled: boolean;
  /* should not show */
  hidden: boolean;
}

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

type FieldUIOptions = {
  placeholder?: TranslatedString;
  tooltip?: TranslatedString;
  help?: TranslatedString;
  required?: boolean;
}

export interface UIFormProps<T extends object, K extends keyof T> extends FieldUIOptions {
  name: K;
  label: TranslatedString;
  before?: Addon;
  after?: Addon;
  converter?: StringConverter<T[K]>;
}

export interface InputArrayFieldState<P extends object> extends BehaviorResult {
  elements?: FormState<P>[];
}

export function FormProvider<T extends object>({
  children,
  initialValue,
  onUpdate: notify,
  onSubmit,
  computeFormState,
  readOnly,
}: {
  initialValue?: Partial<T>;
  onUpdate?: (v: Partial<T>) => void;
  onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
  computeFormState?: (v: Partial<T>) => FormState<T>;
  readOnly?: boolean;
  children: ComponentChildren;
}): VNode {

  const [state, setState] = useState<Partial<T>>(initialValue ?? {});
  const value = { current: state };
  const onUpdate = (v: typeof state) => {
    setState(v);
    if (notify) notify(v);
  };
  return (
    <FormContext.Provider
      value={{ initialValue, 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>
  );
}