@colisweb/rescript-toolkit 4.7.2 → 4.8.1

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.2",
3
+ "version": "4.8.1",
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
+ )
@@ -1,3 +1,5 @@
1
+ open ReactIntl
2
+
1
3
  module type Config = {
2
4
  type field<'a>
3
5
  type state
@@ -5,9 +7,13 @@ module type Config = {
5
7
  let get: (state, field<'a>) => 'a
6
8
  type error
7
9
  }
10
+
11
+ type childFieldError = ReSchema.childFieldError
12
+
8
13
  type fieldState =
9
14
  | Pristine
10
15
  | Valid
16
+ | NestedErrors(array<ReSchema.childFieldError>)
11
17
  | Error(string)
12
18
  type formState<'error> =
13
19
  | Pristine
@@ -53,6 +59,7 @@ module Make = (Config: Config) => {
53
59
  state: state,
54
60
  getFieldState: field => fieldState,
55
61
  getFieldError: field => option<string>,
62
+ getNestedFieldError: (field, int) => array<childFieldError>,
56
63
  handleChange: 'a. (Config.field<'a>, 'a) => unit,
57
64
  arrayPush: 'a. (Config.field<array<'a>>, 'a) => unit,
58
65
  arrayUpdateByIndex: 'a. (~field: Config.field<array<'a>>, ~index: int, 'a) => unit,
@@ -92,20 +99,23 @@ module Make = (Config: Config) => {
92
99
  | OnDemand
93
100
 
94
101
  let getInitialFieldsState: Validation.schema => array<(field, fieldState)> = schema => {
95
- let Validation.Schema(validators) = schema
96
- validators->Belt.Array.map(validator =>
102
+ schema->Belt.Array.map(validator =>
97
103
  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)
104
+ | Validation.IntMin({field}) => (ReSchema.Field(field), (Pristine: fieldState))
105
+ | Validation.True({field}) => (Field(field), Pristine)
106
+ | Validation.False({field}) => (Field(field), Pristine)
107
+ | Validation.IntMax({field}) => (Field(field), Pristine)
108
+ | Validation.FloatMin({field}) => (Field(field), Pristine)
109
+ | Validation.FloatMax({field}) => (Field(field), Pristine)
110
+ | Validation.Email({field}) => (Field(field), Pristine)
111
+ | Validation.NoValidation({field}) => (Field(field), Pristine)
112
+ | Validation.StringNonEmpty({field}) => (Field(field), Pristine)
113
+ | Validation.StringRegExp({field}) => (Field(field), Pristine)
114
+ | Validation.StringMin({field}) => (Field(field), Pristine)
115
+ | Validation.StringMax({field}) => (Field(field), Pristine)
116
+ | Validation.Custom({field}) => (Field(field), Pristine)
117
+ | Validation.OptionNonEmpty({field}) => (Field(field), Pristine)
118
+ | Validation.ArrayNonEmpty({field}) => (Field(field), Pristine)
109
119
  }
110
120
  )
111
121
  }
@@ -126,10 +136,30 @@ module Make = (Config: Config) => {
126
136
  }
127
137
  }
128
138
 
129
- module Provider = {
139
+ module FormProvider = {
130
140
  let make = React.Context.provider(formContext)
131
141
  }
132
142
 
143
+ module Provider = {
144
+ @react.component
145
+ let make = (~id=?, ~className=?, ~children, ~form) => {
146
+ <FormProvider value={form}>
147
+ <form
148
+ ?id
149
+ ?className
150
+ onSubmit={event => {
151
+ ReactEvent.Synthetic.preventDefault(event)
152
+ form.submit()
153
+ }}>
154
+ children
155
+ <Toolkit__Ui_VisuallyHidden>
156
+ <input type_="submit" hidden=true />
157
+ </Toolkit__Ui_VisuallyHidden>
158
+ </form>
159
+ </FormProvider>
160
+ }
161
+ }
162
+
133
163
  module Field = {
134
164
  @react.component
135
165
  let make = (
@@ -146,13 +176,16 @@ module Make = (Config: Config) => {
146
176
 
147
177
  let use = (
148
178
  ~initialState,
149
- ~schema: Validation.schema,
179
+ ~schema=[]: Validation.schema,
150
180
  ~onSubmit,
151
181
  ~onSubmitFail=ignore,
152
- ~i18n=ReSchemaI18n.default,
153
- ~validationStrategy=OnChange,
182
+ ~i18n=?,
183
+ ~validationStrategy=OnDemand,
154
184
  (),
155
185
  ) => {
186
+ let intl = useIntl()
187
+ let i18n = i18n->Option.getWithDefault(Toolkit__FormValidationFunctions.i18n(intl))
188
+
156
189
  let (state, send) = ReactUpdate.useReducerWithMapState(
157
190
  (state, action) => {
158
191
  switch action {
@@ -165,6 +198,13 @@ module Make = (Config: Config) => {
165
198
  state: self.state,
166
199
  raiseSubmitFailed: error => self.send(RaiseSubmitFailed(error)),
167
200
  })
201
+ ->Promise.tap(x =>
202
+ switch x {
203
+ | Ok(_) => self.send(SetFormState(SubmitSucceed))
204
+ | Error(error) => self.send(SetFormState(SubmitFailed(Some(error))))
205
+ }
206
+ )
207
+ ->ignore
168
208
 
169
209
  None
170
210
  },
@@ -180,26 +220,27 @@ module Make = (Config: Config) => {
180
220
  | ValidateField(field) =>
181
221
  SideEffects(
182
222
  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 {
223
+ let fieldState = ReSchema.validateOne(
224
+ ~field,
225
+ ~values=self.state.values,
226
+ ~i18n,
227
+ schema,
228
+ )
229
+ let newFieldState: fieldState = switch fieldState {
230
+ | None => Valid
231
+ | Some(fieldState) =>
232
+ switch fieldState {
187
233
  | (_, Error(message)) => Error(message)
234
+ | (_, NestedErrors(errors)) => NestedErrors(errors)
188
235
  | (_, Valid) => Valid
189
236
  }
190
- )
237
+ }
191
238
 
192
239
  let newFieldsState =
193
240
  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
- )
241
+ ->Belt.Array.keep(((value, _)) => value != field)
242
+ ->Belt.Array.concat([(field, newFieldState)])
243
+
203
244
  self.send(SetFieldsState(newFieldsState))
204
245
  None
205
246
  },
@@ -207,23 +248,38 @@ module Make = (Config: Config) => {
207
248
  | ValidateForm(submit) =>
208
249
  SideEffects(
209
250
  self => {
210
- let recordState = schema |> ReSchema.validate(~i18n, self.state.values)
251
+ let recordState = ReSchema.validate(~i18n, self.state.values, schema)
211
252
 
212
253
  switch recordState {
213
- | Valid =>
214
- self.send(SetFormState(Valid))
215
- submit ? self.send(Submit) : ()
254
+ | Valid => {
255
+ let newFieldsState: array<(field, fieldState)> =
256
+ self.state.fieldsState->Belt.Array.map(((field, _)) => (
257
+ field,
258
+ (Valid: fieldState),
259
+ ))
260
+ self.send(SetFieldsState(newFieldsState))
261
+ submit ? self.send(Submit) : ()
262
+ }
216
263
  | Errors(erroredFields) =>
217
- let newFieldsState =
218
- erroredFields->Belt.Array.map(((field, errorMessage)) => (
219
- field,
220
- Error(errorMessage),
221
- ))
264
+ let newFieldsState: array<(field, fieldState)> = erroredFields->Belt.Array.map(((
265
+ field,
266
+ err,
267
+ )) => (
268
+ field,
269
+ switch err {
270
+ | Error(e) => Error(e)
271
+ | NestedErrors(e) => NestedErrors(e)
272
+ | Valid => Valid
273
+ },
274
+ ))
222
275
  self.send(SetFieldsState(newFieldsState))
223
276
  submit
224
277
  ? onSubmitFail({
225
278
  send: self.send,
226
- state: self.state,
279
+ state: {
280
+ ...self.state,
281
+ fieldsState: newFieldsState,
282
+ },
227
283
  raiseSubmitFailed: error => self.send(RaiseSubmitFailed(error)),
228
284
  })
229
285
  : ()
@@ -328,10 +384,19 @@ module Make = (Config: Config) => {
328
384
  x =>
329
385
  switch x {
330
386
  | Error(error) => Some(error)
387
+ | NestedErrors(_errors) => None
331
388
  | _ => None
332
389
  }
333
390
  )
334
391
 
392
+ let getNestedFieldError = (field, index) =>
393
+ switch getFieldState(field) {
394
+ | NestedErrors(errors) => errors->Array.keep(error => error.index === index)
395
+ | Pristine
396
+ | Valid
397
+ | Error(_) => []
398
+ }
399
+
335
400
  let validateFields = (fields: array<field>) => {
336
401
  let fieldsValidated = ReSchema.validateFields(~fields, ~values=state.values, ~i18n, schema)
337
402
 
@@ -354,6 +419,7 @@ module Make = (Config: Config) => {
354
419
  switch newFieldStateValidated {
355
420
  | Valid => [(field, (Valid: fieldState))]
356
421
  | Error(message) => [(field, Error(message))]
422
+ | NestedErrors(message) => [(field, NestedErrors(message))]
357
423
  }
358
424
 
359
425
  | None => []
@@ -386,6 +452,7 @@ module Make = (Config: Config) => {
386
452
  shouldValidate ? send(FieldChangeValue(field, value)) : send(SetFieldValue(field, value)),
387
453
  getFieldState,
388
454
  getFieldError,
455
+ getNestedFieldError,
389
456
  handleChange: (field, value) => send(FieldChangeValue(field, value)),
390
457
  arrayPush: (field, value) => send(FieldArrayAdd(field, value)),
391
458
  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
  }