@colisweb/rescript-toolkit 4.7.1 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bsconfig.json CHANGED
@@ -28,7 +28,6 @@
28
28
  "reason-promise",
29
29
  "decco",
30
30
  "rescript-classnames",
31
- "reschema",
32
31
  "rescript-react-update",
33
32
  "@colisweb/restorative"
34
33
  ],
package/locale/fr.json CHANGED
@@ -54,11 +54,6 @@
54
54
  "defaultMessage": "Seulement",
55
55
  "message": "Seulement"
56
56
  },
57
- {
58
- "id": "_471931e7",
59
- "defaultMessage": "Le champ est requis.",
60
- "message": "Le champ est requis."
61
- },
62
57
  {
63
58
  "id": "_491a4e71",
64
59
  "defaultMessage": "Tapez au moins {minSearchLength, plural, one {1 caractère} other {# caractères}}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colisweb/rescript-toolkit",
3
- "version": "4.7.1",
3
+ "version": "4.8.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "clean": "rescript clean",
@@ -64,7 +64,6 @@
64
64
  "react-use": "17.4.0",
65
65
  "reason-promise": "1.1.5",
66
66
  "res-react-intl": "3.1.2",
67
- "reschema": "1.3.1",
68
67
  "rescript": "10.1.4",
69
68
  "rescript-classnames": "6.0.0",
70
69
  "rescript-react-update": "5.0.0",
@@ -0,0 +1,322 @@
1
+ module type Lenses = {
2
+ type field<'a>
3
+ type state
4
+ let set: (state, field<'a>, 'a) => state
5
+ let get: (state, field<'a>) => 'a
6
+ }
7
+
8
+ type childFieldError = {
9
+ error: string,
10
+ index: int,
11
+ name: string,
12
+ }
13
+
14
+ type fieldState =
15
+ | Valid
16
+ | NestedErrors(array<childFieldError>)
17
+ | Error(string)
18
+
19
+ type recordValidationState<'a> =
20
+ | Valid
21
+ | Errors(array<('a, fieldState)>)
22
+
23
+ module Make = (Lenses: Lenses) => {
24
+ type rec field = Field(Lenses.field<'a>): field
25
+
26
+ module Validation = {
27
+ type rec t =
28
+ | Email({field: Lenses.field<string>, error: option<string>}): t
29
+ | NoValidation({field: Lenses.field<'a>}): t
30
+ | StringNonEmpty({field: Lenses.field<string>, error: option<string>}): t
31
+ | StringRegExp({field: Lenses.field<string>, matches: string, error: option<string>}): t
32
+ | StringMin({field: Lenses.field<string>, min: int, error: option<string>}): t
33
+ | StringMax({field: Lenses.field<string>, max: int, error: option<string>}): t
34
+ | IntMin({field: Lenses.field<int>, min: int, error: option<string>}): t
35
+ | IntMax({field: Lenses.field<int>, max: int, error: option<string>}): t
36
+ | FloatMin({field: Lenses.field<float>, min: float, error: option<string>}): t
37
+ | FloatMax({field: Lenses.field<float>, max: float, error: option<string>}): t
38
+ | Custom({field: Lenses.field<'a>, predicate: Lenses.state => fieldState}): t
39
+ | True({field: Lenses.field<bool>, error: option<string>}): t
40
+ | False({field: Lenses.field<bool>, error: option<string>}): t
41
+ | OptionNonEmpty({field: Lenses.field<option<'a>>, error: option<string>}): t
42
+ | ArrayNonEmpty({field: Lenses.field<array<'a>>, error: option<string>}): t
43
+
44
+ type schema = array<t>
45
+
46
+ let schema = validations => validations->Belt.Array.concatMany
47
+
48
+ let mergeValidators = validations => {
49
+ validations->Belt.Array.reduce([], (rules, (rule, apply)) =>
50
+ switch rule {
51
+ | None => rules
52
+ | Some(rule) => rules->Belt.Array.concat([rule->apply])
53
+ }
54
+ )
55
+ }
56
+
57
+ let custom = (field, predicate) => [Custom({field, predicate})]
58
+
59
+ let optionNonEmpty = (~error=?, field) => [OptionNonEmpty({field, error})]
60
+
61
+ let arrayNonEmpty = (~error=?, field) => [ArrayNonEmpty({field, error})]
62
+
63
+ let true_ = (~error=?, field) => [True({field, error})]
64
+
65
+ let false_ = (~error=?, field) => [False({field, error})]
66
+
67
+ let email = (~error=?, field) => [Email({field, error})]
68
+
69
+ let nonEmpty = (~error=?, field) => [StringNonEmpty({field, error})]
70
+
71
+ let string = (~min=?, ~minError=?, ~max=?, ~maxError=?, field) => {
72
+ mergeValidators([
73
+ (
74
+ min,
75
+ min => StringMin({
76
+ field,
77
+ min,
78
+ error: minError,
79
+ }),
80
+ ),
81
+ (
82
+ max,
83
+ max => StringMax({
84
+ field,
85
+ max,
86
+ error: maxError,
87
+ }),
88
+ ),
89
+ ])
90
+ }
91
+
92
+ let regExp = (~error=?, ~matches, field) => [StringRegExp({field, matches, error})]
93
+
94
+ let float = (~min=?, ~minError=?, ~max=?, ~maxError=?, field) => {
95
+ mergeValidators([
96
+ (
97
+ min,
98
+ min => FloatMin({
99
+ field,
100
+ min,
101
+ error: minError,
102
+ }),
103
+ ),
104
+ (
105
+ max,
106
+ max => FloatMax({
107
+ field,
108
+ max,
109
+ error: maxError,
110
+ }),
111
+ ),
112
+ ])
113
+ }
114
+
115
+ let int = (~min=?, ~minError=?, ~max=?, ~maxError=?, field) => {
116
+ mergeValidators([
117
+ (
118
+ min,
119
+ min => IntMin({
120
+ field,
121
+ min,
122
+ error: minError,
123
+ }),
124
+ ),
125
+ (
126
+ max,
127
+ max => IntMax({
128
+ field,
129
+ max,
130
+ error: maxError,
131
+ }),
132
+ ),
133
+ ])
134
+ }
135
+ }
136
+
137
+ let validateField = (~validator, ~values, ~i18n: ReSchemaI18n.t): (field, fieldState) =>
138
+ switch validator {
139
+ | Validation.True({field, error}) =>
140
+ let value = Lenses.get(values, field)
141
+ (Field(field), value ? Valid : Error(error->Belt.Option.getWithDefault(i18n.true_())))
142
+ | Validation.False({field, error}) =>
143
+ let value = Lenses.get(values, field)
144
+ (
145
+ Field(field),
146
+ value == false ? Valid : Error(error->Belt.Option.getWithDefault(i18n.false_())),
147
+ )
148
+ | Validation.IntMin({field, min, error}) =>
149
+ let value = Lenses.get(values, field)
150
+ (
151
+ Field(field),
152
+ value >= min ? Valid : Error(error->Belt.Option.getWithDefault(i18n.intMin(~value, ~min))),
153
+ )
154
+ | Validation.IntMax({field, max, error}) =>
155
+ let value = Lenses.get(values, field)
156
+
157
+ (
158
+ Field(field),
159
+ value <= max ? Valid : Error(error->Belt.Option.getWithDefault(i18n.intMax(~value, ~max))),
160
+ )
161
+ | Validation.FloatMin({field, min, error}) =>
162
+ let value = Lenses.get(values, field)
163
+ (
164
+ Field(field),
165
+ value >= min
166
+ ? Valid
167
+ : Error(error->Belt.Option.getWithDefault(i18n.floatMin(~value, ~min))),
168
+ )
169
+ | Validation.FloatMax({field, max, error}) =>
170
+ let value = Lenses.get(values, field)
171
+ (
172
+ Field(field),
173
+ Lenses.get(values, field) <= max
174
+ ? Valid
175
+ : Error(error->Belt.Option.getWithDefault(i18n.floatMax(~value, ~max))),
176
+ )
177
+ | Validation.Email({field, error}) =>
178
+ let value = Lenses.get(values, field)
179
+ (
180
+ Field(field),
181
+ Js.Re.test_(ReSchemaRegExp.email, value)
182
+ ? Valid
183
+ : Error(error->Belt.Option.getWithDefault(i18n.email(~value))),
184
+ )
185
+ | Validation.NoValidation({field}) => (Field(field), Valid)
186
+ | Validation.StringNonEmpty({field, error}) =>
187
+ let value = Lenses.get(values, field)
188
+ (
189
+ Field(field),
190
+ value === ""
191
+ ? Error(error->Belt.Option.getWithDefault(i18n.stringNonEmpty(~value)))
192
+ : Valid,
193
+ )
194
+ | Validation.StringRegExp({field, matches, error}) =>
195
+ let value = Lenses.get(values, field)
196
+ (
197
+ Field(field),
198
+ Js.Re.test_(Js.Re.fromString(matches), value)
199
+ ? Valid
200
+ : Error(error->Belt.Option.getWithDefault(i18n.stringRegExp(~value, ~pattern=matches))),
201
+ )
202
+ | Validation.StringMin({field, min, error}) =>
203
+ let value = Lenses.get(values, field)
204
+ (
205
+ Field(field),
206
+ Js.String.length(value) >= min
207
+ ? Valid
208
+ : Error(error->Belt.Option.getWithDefault(i18n.stringMin(~value, ~min))),
209
+ )
210
+ | Validation.StringMax({field, max, error}) =>
211
+ let value = Lenses.get(values, field)
212
+ (
213
+ Field(field),
214
+ Js.String.length(value) <= max
215
+ ? Valid
216
+ : Error(error->Belt.Option.getWithDefault(i18n.stringMax(~value, ~max))),
217
+ )
218
+ | Validation.Custom({field, predicate}) => (Field(field), predicate(values))
219
+
220
+ | Validation.OptionNonEmpty({field, error}) => {
221
+ let value = Lenses.get(values, field)
222
+
223
+ (
224
+ Field(field),
225
+ value->Option.isNone
226
+ ? Error(error->Belt.Option.getWithDefault(i18n.stringNonEmpty(~value="")))
227
+ : Valid,
228
+ )
229
+ }
230
+ | Validation.ArrayNonEmpty({field, error}) => {
231
+ let value = Lenses.get(values, field)
232
+
233
+ (
234
+ Field(field),
235
+ value->Array.length == 0
236
+ ? Error(error->Belt.Option.getWithDefault(i18n.stringNonEmpty(~value="")))
237
+ : Valid,
238
+ )
239
+ }
240
+ }
241
+
242
+ let getFieldValidator = (~validators, ~fieldName) =>
243
+ validators->Belt.Array.getBy(validator =>
244
+ switch validator {
245
+ | Validation.False({field}) => Field(field) == fieldName
246
+ | Validation.True({field}) => Field(field) == fieldName
247
+ | Validation.IntMin({field}) => Field(field) == fieldName
248
+ | Validation.IntMax({field}) => Field(field) == fieldName
249
+ | Validation.FloatMin({field}) => Field(field) == fieldName
250
+ | Validation.FloatMax({field}) => Field(field) == fieldName
251
+ | Validation.Email({field}) => Field(field) == fieldName
252
+ | Validation.NoValidation({field}) => Field(field) == fieldName
253
+ | Validation.StringNonEmpty({field}) => Field(field) == fieldName
254
+ | Validation.StringRegExp({field}) => Field(field) == fieldName
255
+ | Validation.StringMin({field}) => Field(field) == fieldName
256
+ | Validation.StringMax({field}) => Field(field) == fieldName
257
+ | Validation.Custom({field}) => Field(field) == fieldName
258
+ | Validation.OptionNonEmpty({field}) => Field(field) == fieldName
259
+ | Validation.ArrayNonEmpty({field}) => Field(field) == fieldName
260
+ }
261
+ )
262
+
263
+ let getFieldValidators = (~validators, ~fieldName) =>
264
+ validators->Belt.Array.keep(validator =>
265
+ switch validator {
266
+ | Validation.False({field}) => Field(field) == fieldName
267
+ | Validation.True({field}) => Field(field) == fieldName
268
+ | Validation.IntMin({field}) => Field(field) == fieldName
269
+ | Validation.IntMax({field}) => Field(field) == fieldName
270
+ | Validation.FloatMin({field}) => Field(field) == fieldName
271
+ | Validation.FloatMax({field}) => Field(field) == fieldName
272
+ | Validation.Email({field}) => Field(field) == fieldName
273
+ | Validation.NoValidation({field}) => Field(field) == fieldName
274
+ | Validation.StringNonEmpty({field}) => Field(field) == fieldName
275
+ | Validation.StringRegExp({field}) => Field(field) == fieldName
276
+ | Validation.StringMin({field}) => Field(field) == fieldName
277
+ | Validation.StringMax({field}) => Field(field) == fieldName
278
+ | Validation.Custom({field}) => Field(field) == fieldName
279
+ | Validation.OptionNonEmpty({field}) => Field(field) == fieldName
280
+ | Validation.ArrayNonEmpty({field}) => Field(field) == fieldName
281
+ }
282
+ )
283
+
284
+ let validateOne = (
285
+ ~field: field,
286
+ ~values,
287
+ ~i18n=ReSchemaI18n.default,
288
+ schema: Validation.schema,
289
+ ) => {
290
+ getFieldValidators(~validators=schema, ~fieldName=field)
291
+ ->Belt.Array.map(validator => validateField(~validator, ~values, ~i18n))
292
+ ->Belt.Array.getBy(validation => {
293
+ switch validation {
294
+ | (_, Error(_)) => true
295
+ | _ => false
296
+ }
297
+ })
298
+ }
299
+
300
+ let validateFields = (~fields, ~values, ~i18n, schema: Validation.schema) => {
301
+ Belt.Array.map(fields, field =>
302
+ getFieldValidator(~validators=schema, ~fieldName=field)->Belt.Option.map(validator =>
303
+ validateField(~validator, ~values, ~i18n)
304
+ )
305
+ )
306
+ }
307
+
308
+ let validate = (~i18n=ReSchemaI18n.default, values: Lenses.state, schema: Validation.schema) => {
309
+ let validationList =
310
+ schema->Belt.Array.map(validator => validateField(~validator, ~values, ~i18n))
311
+
312
+ let errors = validationList->Belt.Array.keepMap(((field, fieldState)) =>
313
+ switch fieldState {
314
+ | Error(_) as e => Some((field, e))
315
+ | NestedErrors(_) as e => Some((field, e))
316
+ | _ => None
317
+ }
318
+ )
319
+
320
+ Belt.Array.length(errors) > 0 ? Errors(errors) : Valid
321
+ }
322
+ }
@@ -0,0 +1,34 @@
1
+ type t = {
2
+ false_: unit => string,
3
+ true_: unit => string,
4
+ intMin: (~value: int, ~min: int) => string,
5
+ intMax: (~value: int, ~max: int) => string,
6
+ floatMin: (~value: float, ~min: float) => string,
7
+ floatMax: (~value: float, ~max: float) => string,
8
+ email: (~value: string) => string,
9
+ stringNonEmpty: (~value: string) => string,
10
+ stringRegExp: (~value: string, ~pattern: string) => string,
11
+ stringMin: (~value: string, ~min: int) => string,
12
+ stringMax: (~value: string, ~max: int) => string,
13
+ }
14
+
15
+ let default = {
16
+ false_: () => "This value should be false",
17
+ true_: () => "This value should be true",
18
+ intMin: (~value as _value, ~min) =>
19
+ "This value must be greater than or equal to " ++ string_of_int(min),
20
+ intMax: (~value as _value, ~max) =>
21
+ "This value must be less than or equal to " ++ string_of_int(max),
22
+ floatMin: (~value as _value, ~min) =>
23
+ "This value must be greater than or equal to " ++ Js.Float.toString(min),
24
+ floatMax: (~value as _value, ~max) =>
25
+ "This value must be less than or equal to " ++ Js.Float.toString(max),
26
+ email: (~value) => value ++ " is not a valid email",
27
+ stringNonEmpty: (~value as _) => "String must not be empty",
28
+ stringRegExp: (~value as _value, ~pattern) =>
29
+ "This value must match the following: /" ++ pattern ++ "/",
30
+ stringMin: (~value as _value, ~min) =>
31
+ "This value must be at least " ++ string_of_int(min) ++ " characters",
32
+ stringMax: (~value as _value, ~max) =>
33
+ "This value must be at most " ++ string_of_int(max) ++ " characters",
34
+ }
@@ -0,0 +1,5 @@
1
+ // From https://github.com/jquense/yup/blob/92668947dafc2bb60bb7565c53b39091a6213167/src/string.ts#L22
2
+
3
+ let email = %re(
4
+ "/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/"
5
+ )
@@ -5,9 +5,13 @@ module type Config = {
5
5
  let get: (state, field<'a>) => 'a
6
6
  type error
7
7
  }
8
+
9
+ type childFieldError = ReSchema.childFieldError
10
+
8
11
  type fieldState =
9
12
  | Pristine
10
13
  | Valid
14
+ | NestedErrors(array<ReSchema.childFieldError>)
11
15
  | Error(string)
12
16
  type formState<'error> =
13
17
  | Pristine
@@ -53,6 +57,7 @@ module Make = (Config: Config) => {
53
57
  state: state,
54
58
  getFieldState: field => fieldState,
55
59
  getFieldError: field => option<string>,
60
+ getNestedFieldError: (field, int) => array<childFieldError>,
56
61
  handleChange: 'a. (Config.field<'a>, 'a) => unit,
57
62
  arrayPush: 'a. (Config.field<array<'a>>, 'a) => unit,
58
63
  arrayUpdateByIndex: 'a. (~field: Config.field<array<'a>>, ~index: int, 'a) => unit,
@@ -92,20 +97,23 @@ module Make = (Config: Config) => {
92
97
  | OnDemand
93
98
 
94
99
  let getInitialFieldsState: Validation.schema => array<(field, fieldState)> = schema => {
95
- let Validation.Schema(validators) = schema
96
- validators->Belt.Array.map(validator =>
100
+ schema->Belt.Array.map(validator =>
97
101
  switch validator {
98
- | Validation.IntMin(field, _min) => (ReSchema.Field(field), (Pristine: fieldState))
99
- | Validation.IntMax(field, _max) => (Field(field), Pristine)
100
- | Validation.FloatMin(field, _min) => (Field(field), Pristine)
101
- | Validation.FloatMax(field, _max) => (Field(field), Pristine)
102
- | Validation.Email(field) => (Field(field), Pristine)
103
- | Validation.NoValidation(field) => (Field(field), Pristine)
104
- | Validation.StringNonEmpty(field) => (Field(field), Pristine)
105
- | Validation.StringRegExp(field, _regexp) => (Field(field), Pristine)
106
- | Validation.StringMin(field, _min) => (Field(field), Pristine)
107
- | Validation.StringMax(field, _max) => (Field(field), Pristine)
108
- | Validation.Custom(field, _predicate) => (Field(field), Pristine)
102
+ | Validation.IntMin({field}) => (ReSchema.Field(field), (Pristine: fieldState))
103
+ | Validation.True({field}) => (Field(field), Pristine)
104
+ | Validation.False({field}) => (Field(field), Pristine)
105
+ | Validation.IntMax({field}) => (Field(field), Pristine)
106
+ | Validation.FloatMin({field}) => (Field(field), Pristine)
107
+ | Validation.FloatMax({field}) => (Field(field), Pristine)
108
+ | Validation.Email({field}) => (Field(field), Pristine)
109
+ | Validation.NoValidation({field}) => (Field(field), Pristine)
110
+ | Validation.StringNonEmpty({field}) => (Field(field), Pristine)
111
+ | Validation.StringRegExp({field}) => (Field(field), Pristine)
112
+ | Validation.StringMin({field}) => (Field(field), Pristine)
113
+ | Validation.StringMax({field}) => (Field(field), Pristine)
114
+ | Validation.Custom({field}) => (Field(field), Pristine)
115
+ | Validation.OptionNonEmpty({field}) => (Field(field), Pristine)
116
+ | Validation.ArrayNonEmpty({field}) => (Field(field), Pristine)
109
117
  }
110
118
  )
111
119
  }
@@ -126,10 +134,30 @@ module Make = (Config: Config) => {
126
134
  }
127
135
  }
128
136
 
129
- module Provider = {
137
+ module FormProvider = {
130
138
  let make = React.Context.provider(formContext)
131
139
  }
132
140
 
141
+ module Provider = {
142
+ @react.component
143
+ let make = (~id=?, ~className=?, ~children, ~form) => {
144
+ <FormProvider value={form}>
145
+ <form
146
+ ?id
147
+ ?className
148
+ onSubmit={event => {
149
+ ReactEvent.Synthetic.preventDefault(event)
150
+ form.submit()
151
+ }}>
152
+ children
153
+ <Toolkit__Ui_VisuallyHidden>
154
+ <input type_="submit" hidden=true />
155
+ </Toolkit__Ui_VisuallyHidden>
156
+ </form>
157
+ </FormProvider>
158
+ }
159
+ }
160
+
133
161
  module Field = {
134
162
  @react.component
135
163
  let make = (
@@ -146,11 +174,11 @@ module Make = (Config: Config) => {
146
174
 
147
175
  let use = (
148
176
  ~initialState,
149
- ~schema: Validation.schema,
177
+ ~schema=[]: Validation.schema,
150
178
  ~onSubmit,
151
179
  ~onSubmitFail=ignore,
152
180
  ~i18n=ReSchemaI18n.default,
153
- ~validationStrategy=OnChange,
181
+ ~validationStrategy=OnDemand,
154
182
  (),
155
183
  ) => {
156
184
  let (state, send) = ReactUpdate.useReducerWithMapState(
@@ -165,6 +193,13 @@ module Make = (Config: Config) => {
165
193
  state: self.state,
166
194
  raiseSubmitFailed: error => self.send(RaiseSubmitFailed(error)),
167
195
  })
196
+ ->Promise.tap(x =>
197
+ switch x {
198
+ | Ok(_) => self.send(SetFormState(SubmitSucceed))
199
+ | Error(error) => self.send(SetFormState(SubmitFailed(Some(error))))
200
+ }
201
+ )
202
+ ->ignore
168
203
 
169
204
  None
170
205
  },
@@ -180,26 +215,27 @@ module Make = (Config: Config) => {
180
215
  | ValidateField(field) =>
181
216
  SideEffects(
182
217
  self => {
183
- let fieldState =
184
- schema |> ReSchema.validateOne(~field, ~values=self.state.values, ~i18n)
185
- let newFieldState: option<fieldState> = fieldState->Belt.Option.map(x =>
186
- switch x {
218
+ let fieldState = ReSchema.validateOne(
219
+ ~field,
220
+ ~values=self.state.values,
221
+ ~i18n,
222
+ schema,
223
+ )
224
+ let newFieldState: fieldState = switch fieldState {
225
+ | None => Valid
226
+ | Some(fieldState) =>
227
+ switch fieldState {
187
228
  | (_, Error(message)) => Error(message)
229
+ | (_, NestedErrors(errors)) => NestedErrors(errors)
188
230
  | (_, Valid) => Valid
189
231
  }
190
- )
232
+ }
191
233
 
192
234
  let newFieldsState =
193
235
  state.fieldsState
194
- ->Belt.Array.keep(elem =>
195
- elem |> (((fieldValue, _fieldState)) => fieldValue != field)
196
- )
197
- ->Belt.Array.concat(
198
- switch newFieldState {
199
- | Some(fieldState) => [(field, fieldState)]
200
- | None => []
201
- },
202
- )
236
+ ->Belt.Array.keep(((value, _)) => value != field)
237
+ ->Belt.Array.concat([(field, newFieldState)])
238
+
203
239
  self.send(SetFieldsState(newFieldsState))
204
240
  None
205
241
  },
@@ -207,23 +243,38 @@ module Make = (Config: Config) => {
207
243
  | ValidateForm(submit) =>
208
244
  SideEffects(
209
245
  self => {
210
- let recordState = schema |> ReSchema.validate(~i18n, self.state.values)
246
+ let recordState = ReSchema.validate(~i18n, self.state.values, schema)
211
247
 
212
248
  switch recordState {
213
- | Valid =>
214
- self.send(SetFormState(Valid))
215
- submit ? self.send(Submit) : ()
249
+ | Valid => {
250
+ let newFieldsState: array<(field, fieldState)> =
251
+ self.state.fieldsState->Belt.Array.map(((field, _)) => (
252
+ field,
253
+ (Valid: fieldState),
254
+ ))
255
+ self.send(SetFieldsState(newFieldsState))
256
+ submit ? self.send(Submit) : ()
257
+ }
216
258
  | Errors(erroredFields) =>
217
- let newFieldsState =
218
- erroredFields->Belt.Array.map(((field, errorMessage)) => (
219
- field,
220
- Error(errorMessage),
221
- ))
259
+ let newFieldsState: array<(field, fieldState)> = erroredFields->Belt.Array.map(((
260
+ field,
261
+ err,
262
+ )) => (
263
+ field,
264
+ switch err {
265
+ | Error(e) => Error(e)
266
+ | NestedErrors(e) => NestedErrors(e)
267
+ | Valid => Valid
268
+ },
269
+ ))
222
270
  self.send(SetFieldsState(newFieldsState))
223
271
  submit
224
272
  ? onSubmitFail({
225
273
  send: self.send,
226
- state: self.state,
274
+ state: {
275
+ ...self.state,
276
+ fieldsState: newFieldsState,
277
+ },
227
278
  raiseSubmitFailed: error => self.send(RaiseSubmitFailed(error)),
228
279
  })
229
280
  : ()
@@ -328,10 +379,19 @@ module Make = (Config: Config) => {
328
379
  x =>
329
380
  switch x {
330
381
  | Error(error) => Some(error)
382
+ | NestedErrors(_errors) => None
331
383
  | _ => None
332
384
  }
333
385
  )
334
386
 
387
+ let getNestedFieldError = (field, index) =>
388
+ switch getFieldState(field) {
389
+ | NestedErrors(errors) => errors->Array.keep(error => error.index === index)
390
+ | Pristine
391
+ | Valid
392
+ | Error(_) => []
393
+ }
394
+
335
395
  let validateFields = (fields: array<field>) => {
336
396
  let fieldsValidated = ReSchema.validateFields(~fields, ~values=state.values, ~i18n, schema)
337
397
 
@@ -354,6 +414,7 @@ module Make = (Config: Config) => {
354
414
  switch newFieldStateValidated {
355
415
  | Valid => [(field, (Valid: fieldState))]
356
416
  | Error(message) => [(field, Error(message))]
417
+ | NestedErrors(message) => [(field, NestedErrors(message))]
357
418
  }
358
419
 
359
420
  | None => []
@@ -386,6 +447,7 @@ module Make = (Config: Config) => {
386
447
  shouldValidate ? send(FieldChangeValue(field, value)) : send(SetFieldValue(field, value)),
387
448
  getFieldState,
388
449
  getFieldError,
450
+ getNestedFieldError,
389
451
  handleChange: (field, value) => send(FieldChangeValue(field, value)),
390
452
  arrayPush: (field, value) => send(FieldArrayAdd(field, value)),
391
453
  arrayUpdateByIndex: (~field, ~index, value) =>
@@ -1,19 +1,5 @@
1
1
  open ReactIntl
2
2
 
3
- module Helpers = {
4
- let handleChange = (handleChange, event) => handleChange(ReactEvent.Form.target(event)["value"])
5
-
6
- let handleSubmit = (handleSubmit, event) => {
7
- ReactEvent.Synthetic.preventDefault(event)
8
- handleSubmit()
9
- }
10
- }
11
-
12
- module Msg = {
13
- @@intl.messages
14
- let optionNonEmpty = {defaultMessage: "Le champ est requis."}
15
- }
16
-
17
3
  module ErrorMessage = {
18
4
  @react.component
19
5
  let make = (~error=?, ~className="") =>
@@ -33,83 +19,12 @@ module type Config = {
33
19
  }
34
20
 
35
21
  module Make = (StateLenses: Config) => {
36
- module Form = {
37
- include Reform.Make(StateLenses)
38
-
39
- let use = (
40
- ~initialState,
41
- ~schema=Validation.Schema([]),
42
- ~onSubmit=?,
43
- ~onSubmitFail=ignore,
44
- ~validationStrategy=OnDemand,
45
- (),
46
- ): api => {
47
- let intl = useIntl()
48
-
49
- use(
50
- ~initialState,
51
- ~schema,
52
- ~onSubmit=form => {
53
- onSubmit
54
- ->Option.map(onSubmit =>
55
- onSubmit(form)->Promise.tap(x =>
56
- switch x {
57
- | Ok(_) => form.send(SetFormState(SubmitSucceed))
58
- | Error(error) => form.send(SetFormState(SubmitFailed(Some(error))))
59
- }
60
- )
61
- )
62
- ->ignore
63
- },
64
- ~onSubmitFail,
65
- ~i18n=Toolkit__FormValidationFunctions.i18n(intl),
66
- ~validationStrategy,
67
- (),
68
- )
69
- }
70
- }
71
-
72
- module CustomValidation = {
73
- let optionNonEmpty = (intl, lense) => Form.Validation.Custom(
74
- lense,
75
- values =>
76
- values->StateLenses.get(lense)->Option.isNone
77
- ? Error(intl->Intl.formatMessage(Msg.optionNonEmpty))
78
- : Valid,
79
- )
80
-
81
- let arrayNonEmpty = (intl, lense) => Form.Validation.Custom(
82
- lense,
83
- values =>
84
- values->StateLenses.get(lense)->Array.length == 0
85
- ? Error(intl->Intl.formatMessage(Msg.optionNonEmpty))
86
- : Valid,
87
- )
88
- }
89
-
90
- module Wrapper = {
91
- @react.component
92
- let make = (~id=?, ~className=?, ~children) => {
93
- let form = Form.useFormContext()
94
- <form
95
- ?id
96
- ?className
97
- onSubmit={event => {
98
- ReactEvent.Synthetic.preventDefault(event)
99
- form.submit()
100
- }}>
101
- children
102
- <Toolkit__Ui_VisuallyHidden>
103
- <input type_="submit" hidden=true />
104
- </Toolkit__Ui_VisuallyHidden>
105
- </form>
106
- }
107
- }
22
+ include Reform.Make(StateLenses)
108
23
 
109
24
  module RadioGroup = {
110
25
  @react.component
111
26
  let make = (~field, ~elements, ~inline=?) =>
112
- <Form.Field
27
+ <Field
113
28
  field
114
29
  render={({handleChange, error, value}) => <>
115
30
  <Toolkit__Ui_RadioGroup
@@ -148,7 +63,7 @@ module Make = (StateLenses: Config) => {
148
63
  let (showPassword, setShowPassword) = React.useState(() => false)
149
64
  let isPasswordType = type_->Option.mapWithDefault(false, type_ => type_ === "password")
150
65
 
151
- <Form.Field
66
+ <Field
152
67
  field
153
68
  key={showPassword->string_of_bool}
154
69
  render={({handleChange, error, value, validate, state}) => {
@@ -256,7 +171,7 @@ module Make = (StateLenses: Config) => {
256
171
  }
257
172
  `)
258
173
 
259
- <Form.Field
174
+ <Field
260
175
  field
261
176
  render={({handleChange, error, value, validate, state}) => {
262
177
  let isInvalid = error->Option.isSome
@@ -330,7 +245,7 @@ module Make = (StateLenses: Config) => {
330
245
  ~timeIntervals=?,
331
246
  ~inline=?,
332
247
  ) =>
333
- <Form.Field
248
+ <Field
334
249
  field
335
250
  render={({handleChange, error, value, validate, state}) => {
336
251
  let isInvalid = error->Option.isSome
@@ -399,12 +314,12 @@ module Make = (StateLenses: Config) => {
399
314
  ~isOptional=?,
400
315
  ~className=?,
401
316
  ) =>
402
- <Form.Field
317
+ <Field
403
318
  field
404
319
  render={({handleChange, error, value, validate, state}) => {
405
320
  let isInvalid = error->Option.isSome
406
321
 
407
- let onChange = Helpers.handleChange(handleChange)
322
+ let onChange = event => handleChange(ReactEvent.Form.target(event)["value"])
408
323
 
409
324
  let onBlur = _ =>
410
325
  switch state {
@@ -446,7 +361,7 @@ module Make = (StateLenses: Config) => {
446
361
  module Checkbox = {
447
362
  @react.component
448
363
  let make = (~field, ~label, ~name=?, ~disabled=?, ~className=?) =>
449
- <Form.Field
364
+ <Field
450
365
  field
451
366
  render={({handleChange, error, value}) => <>
452
367
  <Toolkit__Ui_Checkbox
@@ -477,7 +392,7 @@ module Make = (StateLenses: Config) => {
477
392
  ~isOptional=?,
478
393
  ~className=?,
479
394
  ) => {
480
- <Form.Field
395
+ <Field
481
396
  field
482
397
  render={({handleChange, error, value, validate, state}) => {
483
398
  let isInvalid = error->Option.isSome
@@ -539,7 +454,7 @@ module Make = (StateLenses: Config) => {
539
454
  ~isOptional=?,
540
455
  ~className=?,
541
456
  ) => {
542
- <Form.Field
457
+ <Field
543
458
  field
544
459
  render={({handleChange, error, value, validate, state}) => {
545
460
  let isInvalid = error->Option.isSome
@@ -604,7 +519,7 @@ module Make = (StateLenses: Config) => {
604
519
  ~valueFormat,
605
520
  ~autoFocus=?,
606
521
  ) => {
607
- <Form.Field
522
+ <Field
608
523
  field
609
524
  render={({handleChange, error, value}) => {
610
525
  let isInvalid = error->Option.isSome
@@ -654,7 +569,7 @@ module Make = (StateLenses: Config) => {
654
569
  ~dropdownClassName=?,
655
570
  ~itemClassName=?,
656
571
  ) => {
657
- <Form.Field
572
+ <Field
658
573
  field
659
574
  render={({handleChange, error, value, validate, state}) => {
660
575
  let onClose = _ => {
@@ -705,7 +620,7 @@ module Make = (StateLenses: Config) => {
705
620
  ~switchClassName=?,
706
621
  ~labelClassName=?,
707
622
  ) =>
708
- <Form.Field
623
+ <Field
709
624
  field
710
625
  render={({handleChange, value}) => <>
711
626
  <Toolkit__Ui_Switch
@@ -731,6 +646,7 @@ module Make = (StateLenses: Config) => {
731
646
  ~containerClassName,
732
647
  ~placeholder,
733
648
  ~disabledBefore: option<Js.Date.t>=?,
649
+ ~revalidate,
734
650
  ) => {
735
651
  let (date, setDate) = React.useState((): option<Js.Date.t> => value)
736
652
  let intl = useIntl()
@@ -800,6 +716,7 @@ module Make = (StateLenses: Config) => {
800
716
  type_="button"
801
717
  disabled={date->Option.isNone}
802
718
  onClick={_ => {
719
+ revalidate()
803
720
  setDate(_ => None)
804
721
  }}>
805
722
  <FormattedMessage defaultMessage={"Réinitialiser"} />
@@ -810,6 +727,7 @@ module Make = (StateLenses: Config) => {
810
727
  disabled={date->Option.isNone}
811
728
  onClick={_ => {
812
729
  disclosure.hide()
730
+ revalidate()
813
731
  handleChange(date)
814
732
  }}>
815
733
  <FormattedMessage defaultMessage={"Choisir"} />
@@ -831,23 +749,32 @@ module Make = (StateLenses: Config) => {
831
749
  ~placeholder=?,
832
750
  ~disabledBefore=?,
833
751
  ) => {
834
- <Form.Field
752
+ <Field
835
753
  field
836
- render={({handleChange, value, error}) => <>
837
- {switch label {
838
- | None => React.null
839
- | Some(label) =>
840
- <Toolkit__Ui_Label
841
- htmlFor=id
842
- optionalMessage={isOptional->Option.getWithDefault(false)
843
- ? <FormattedMessage defaultMessage="(Optionnel)" />
844
- : React.null}>
845
- label
846
- </Toolkit__Ui_Label>
847
- }}
848
- <Picker value handleChange containerClassName placeholder ?disabledBefore />
849
- <ErrorMessage ?error />
850
- </>}
754
+ render={({handleChange, value, error, state, validate}) => {
755
+ let revalidate = () => {
756
+ switch state {
757
+ | Pristine => ()
758
+ | _ => validate()
759
+ }
760
+ }
761
+
762
+ <>
763
+ {switch label {
764
+ | None => React.null
765
+ | Some(label) =>
766
+ <Toolkit__Ui_Label
767
+ htmlFor=id
768
+ optionalMessage={isOptional->Option.getWithDefault(false)
769
+ ? <FormattedMessage defaultMessage="(Optionnel)" />
770
+ : React.null}>
771
+ label
772
+ </Toolkit__Ui_Label>
773
+ }}
774
+ <Picker value handleChange containerClassName placeholder ?disabledBefore revalidate />
775
+ <ErrorMessage ?error />
776
+ </>
777
+ }}
851
778
  />
852
779
  }
853
780
  }
@@ -45,13 +45,13 @@ let make = (
45
45
  | #lg => "w-10 h-10"
46
46
  },
47
47
  ])}>
48
- <ReactIcons.GoPrimitiveDot
48
+ <ReactIcons.FaCircle
49
49
  className="transform transition-all ease-in-out"
50
50
  size={switch size {
51
- | #xs => 12
52
- | #sm => 16
53
- | #md => 24
54
- | #lg => 32
51
+ | #xs => 8
52
+ | #sm => 12
53
+ | #md => 18
54
+ | #lg => 24
55
55
  }}
56
56
  />
57
57
  </span>