Skip to content

Commit fb59e9a

Browse files
committed
Implement serializer for Field
1 parent 01b5872 commit fb59e9a

File tree

2 files changed

+148
-10
lines changed

2 files changed

+148
-10
lines changed

src/elements/Field.tsx

Lines changed: 145 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,152 @@ export type ElementProps<C extends React.FunctionComponent<any> | keyof JSX.Intr
88
? JSX.IntrinsicElements[C]
99
: never;
1010

11+
export type FieldProps<T extends object, K extends keyof T, C> = {
12+
form: FormState<T>;
13+
name: K;
14+
as?: C;
15+
serializer?: Serializer<T[K]>;
16+
deserializer?: Deserializer<T[K]>;
17+
};
18+
1119
export function Field<T extends object, K extends keyof T, C extends React.FunctionComponent<any> | keyof JSX.IntrinsicElements = "input">(
12-
props: {
13-
form: FormState<T>;
14-
name: K;
15-
as?: C;
16-
} & Omit<ElementProps<C>, "form" | "name" | "as" | "value" | "onChange">
20+
props: FieldProps<T, K, C> & Omit<ElementProps<C>, "value" | "onChange" | keyof FieldProps<T, K, C> | keyof SerializeProps> & SerializeProps
1721
) {
18-
const { form, as = "input", ...rest } = props;
22+
const { form, as = "input", serializer, dateAsNumber, setNullOnUncheck, setUndefinedOnUncheck, deserializer, ...rest } = props;
23+
const serializeProps = {
24+
dateAsNumber,
25+
setNullOnUncheck,
26+
setUndefinedOnUncheck,
27+
type: props.type,
28+
value: props.value
29+
};
1930
const { value, setValue } = useListener(form, props.name);
20-
const onChange = useCallback((ev: any) => setValue(ev.target?.value ?? ev), [setValue]);
21-
return React.createElement(as, { ...rest, value, onChange });
31+
const onChange = useCallback(
32+
(ev: any) => {
33+
let [v, c] = "target" in ev ? [ev.target.value, ev.target.checked] : [ev, typeof ev === "boolean" ? ev : undefined];
34+
setValue((deserializer ?? defaultDeserializer)(v, c, value, serializeProps));
35+
},
36+
[setValue]
37+
);
38+
let v = (serializer ?? defaultSerializer)(value, serializeProps);
39+
return React.createElement(as, {
40+
...rest,
41+
checked: typeof v === "boolean" ? v : undefined,
42+
value: typeof v === "boolean" ? undefined : value,
43+
onChange
44+
});
45+
}
46+
47+
export type SerializeType =
48+
| "number"
49+
| "text"
50+
| "password"
51+
| "date"
52+
| "datetime-local"
53+
| "radio"
54+
| "checkbox"
55+
| "color"
56+
| "email"
57+
| "text"
58+
| "month"
59+
| "url"
60+
| "week"
61+
| "time"
62+
| "tel"
63+
| "range";
64+
65+
export type Serializer<T> = (currentValue: T, props: SerializeProps<T>) => boolean | string;
66+
export type Deserializer<T> = (inputValue: string, inputChecked: boolean, currentValue: T, props: SerializeProps<T>) => T;
67+
68+
export type SerializeProps<V = any> = {
69+
dateAsNumber?: boolean;
70+
setUndefinedOnUncheck?: boolean;
71+
setNullOnUncheck?: boolean;
72+
type?: SerializeType;
73+
value?: V;
74+
};
75+
76+
export function defaultSerializer<T>(currentValue: T, props: SerializeProps<T>): boolean | string {
77+
console.log("serialize", currentValue, props);
78+
switch (props.type) {
79+
case "datetime-local":
80+
case "date": {
81+
let dateValue = currentValue as any;
82+
if (typeof dateValue === "string") {
83+
let ni = parseInt(dateValue);
84+
if (!isNaN(ni)) dateValue = ni;
85+
}
86+
let date = new Date(dateValue);
87+
if (!isNaN(date.getTime())) {
88+
return date?.toISOString().split("T")[0] ?? "";
89+
} else {
90+
return "";
91+
}
92+
}
93+
case "radio": {
94+
return currentValue === props.value;
95+
}
96+
case "checkbox": {
97+
if (props.setNullOnUncheck) {
98+
return currentValue !== null;
99+
} else if (props.setUndefinedOnUncheck) {
100+
return currentValue !== undefined;
101+
} else if (props.value !== undefined) {
102+
return (Array.isArray(currentValue) ? currentValue : []).includes(props.value as never);
103+
} else {
104+
return !!currentValue;
105+
}
106+
}
107+
default: {
108+
return (currentValue ?? "") + "";
109+
}
110+
}
111+
}
112+
113+
export function defaultDeserializer<T>(inputValue: string, inputChecked: boolean, currentValue: T, props: SerializeProps<T>) {
114+
console.log("deserialize", inputValue, inputChecked, props);
115+
switch (props.type) {
116+
case "number": {
117+
return parseFloat(inputValue) as any;
118+
}
119+
case "datetime-local":
120+
case "date": {
121+
if (inputValue) {
122+
let d = new Date(inputValue);
123+
return (props.dateAsNumber ? d.getTime() : d) as any;
124+
} else {
125+
return null as any;
126+
}
127+
}
128+
case "radio": {
129+
// Enum field
130+
if (inputChecked) {
131+
return props.value as any;
132+
}
133+
return currentValue;
134+
}
135+
case "checkbox": {
136+
if (props.setNullOnUncheck || props.setUndefinedOnUncheck) {
137+
if (inputChecked && props.value === undefined && process.env.NODE_ENV === "development") {
138+
console.error(
139+
"Checkbox using setNullOnUncheck got checked but a value to set was not found, please provide a value to the value prop."
140+
);
141+
}
142+
return inputChecked ? props.value : ((props.setNullOnUncheck ? null : undefined) as any);
143+
} else if (props.value !== undefined) {
144+
// Primitive array field
145+
let arr = Array.isArray(currentValue) ? [...currentValue] : [];
146+
if (inputChecked) arr.push(props.value);
147+
else arr.splice(arr.indexOf(props.value), 1);
148+
return arr as any;
149+
} else {
150+
// Boolean field
151+
return inputChecked as any;
152+
}
153+
}
154+
default: {
155+
// String field
156+
return inputValue as any;
157+
}
158+
}
22159
}

testing/src/Fieldform.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ function Error(props: { children: React.ReactNode }) {
2121
}
2222

2323
export function FieldForm() {
24-
const form = useForm({ email: "", firstName: "", gender: "male" as "male" | "female" }, validate);
24+
const form = useForm({ email: "", firstName: "", gender: "male" as "male" | "female", enableEmail: true }, validate);
2525

2626
function submit() {
2727
console.log("this is epic");
2828
}
2929

3030
return (
3131
<form onSubmit={form.handleSubmit(submit)}>
32-
<Field form={form} name="email" as="input" />
32+
<Field form={form} name="firstName" as="input" />
33+
<Field form={form} name="enableEmail" type="checkbox" />
3334
<Field form={form} name="email" style={{ margin: "2em" }} />
3435
<Field form={form} name="email" as={Input} style={{ margin: "2em" }} />
3536
<FieldError form={form} name="email" as={Error} />

0 commit comments

Comments
 (0)