@@ -8,15 +8,152 @@ export type ElementProps<C extends React.FunctionComponent<any> | keyof JSX.Intr
8
8
? JSX . IntrinsicElements [ C ]
9
9
: never ;
10
10
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
+
11
19
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
17
21
) {
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
+ } ;
19
30
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
+ }
22
159
}
0 commit comments