@conform-to/react 0.1.0 → 0.3.0-pre.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/README.md CHANGED
@@ -1,21 +1,566 @@
1
1
  # @conform-to/react
2
2
 
3
- > View adapter for [react](https://github.com/facebook/react), built on top of `@conform-to/dom`
3
+ > [React](https://github.com/facebook/react) adapter for [conform](https://github.com/edmundhung/conform)
4
4
 
5
5
  ## API Reference
6
6
 
7
- - [conform](#conform)
8
- - [useControlledInput](#useControlledInput)
9
7
  - [useForm](#useForm)
10
8
  - [useFieldset](#useFieldset)
11
9
  - [useFieldList](#useFieldList)
10
+ - [useControlledInput](#useControlledInput)
11
+ - [conform](#conform)
12
12
 
13
- ### conform
14
-
15
- ### useControlledInput
13
+ ---
16
14
 
17
15
  ### useForm
18
16
 
17
+ By default, the browser calls the [reportValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity) API on the form element when it is submitted. This checks the validity of all the fields in it and reports if there are errors through the bubbles.
18
+
19
+ This hook enhances the form validation behaviour in 3 parts:
20
+
21
+ 1. It lets you hook up custom validation logic into different form events. For example, revalidation will be triggered whenever something changed.
22
+ 2. It provides options for you to decide the best timing to start reporting errors. This could be as earliest as the user start typing, or also as late as the user try submitting the form.
23
+ 3. It exposes the state of each field in the form of [data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*), such as `data-conform-touched`, allowing flexible styling across your form without the need to manipulate the class names.
24
+
25
+ ```tsx
26
+ import { useForm } from '@conform-to/react';
27
+
28
+ function LoginForm() {
29
+ const formProps = useForm({
30
+ /**
31
+ * Define when the error should be reported initially.
32
+ * Support "onSubmit", "onChange", "onBlur".
33
+ *
34
+ * Default to `onSubmit`.
35
+ */
36
+ initialReport: 'onBlur',
37
+
38
+ /**
39
+ * Enable native validation before hydation.
40
+ *
41
+ * Default to `false`.
42
+ */
43
+ fallbackNative: false,
44
+
45
+ /**
46
+ * Accept form submission regardless of the form validity.
47
+ *
48
+ * Default to `false`.
49
+ */
50
+ noValidate: false,
51
+
52
+ /**
53
+ * A function to be called when the form should be (re)validated.
54
+ */
55
+ validate(form, submitter) {
56
+ // ...
57
+ },
58
+
59
+ /**
60
+ * The submit event handler of the form.
61
+ */
62
+ onSubmit(event) {
63
+ // ...
64
+ },
65
+ });
66
+
67
+ return (
68
+ <form {...formProps}>
69
+ <input type="email" name="email" required />
70
+ <input type="password" name="password" required />
71
+ <button type="submit">Login</button>
72
+ </form>
73
+ );
74
+ }
75
+ ```
76
+
77
+ <details>
78
+ <summary>What is `formProps`?</summary>
79
+
80
+ It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below:
81
+
82
+ ```tsx
83
+ function RandomForm() {
84
+ const formProps = useForm();
85
+
86
+ return (
87
+ <form
88
+ ref={formProps.ref}
89
+ onSubmit={formProps.onSubmit}
90
+ noValidate={formProps.noValidate}
91
+ >
92
+ {/* ... */}
93
+ </form>
94
+ );
95
+ }
96
+ ```
97
+
98
+ </details>
99
+
100
+ <details>
101
+ <summary>Does it work with custom form component like Remix Form?</summary>
102
+
103
+ Yes! It will fallback to native form submission if the submit event handler is omitted or the event is not default prevented.
104
+
105
+ ```tsx
106
+ import { useFrom } from '@conform-to/react';
107
+ import { Form } from '@remix-run/react';
108
+
109
+ function LoginForm() {
110
+ const formProps = useForm();
111
+
112
+ return (
113
+ <Form method="post" action="/login" {...formProps}>
114
+ {/* ... */}
115
+ </Form>
116
+ );
117
+ }
118
+ ```
119
+
120
+ </details>
121
+
122
+ <details>
123
+ <summary>Is the `validate` function required?</summary>
124
+
125
+ The `validate` function is not required if the validation logic can be fully covered by the [native constraints](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Constraint_validation#validation-related_attributes), e.g. **required** / **min** / **pattern** etc.
126
+
127
+ ```tsx
128
+ import { useForm, useFieldset } from '@conform-to/react';
129
+
130
+ function LoginForm() {
131
+ const formProps = useForm();
132
+ const { email, password } = useFieldset(formProps.ref);
133
+
134
+ return (
135
+ <form {...formProps}>
136
+ <label>
137
+ <input type="email" name="email" required />
138
+ {email.error}
139
+ </label>
140
+ <label>
141
+ <input type="password" name="password" required />
142
+ {password.error}
143
+ </label>
144
+ <button type="submit">Login</button>
145
+ </form>
146
+ );
147
+ }
148
+ ```
149
+
150
+ </details>
151
+
152
+ ---
153
+
19
154
  ### useFieldset
20
155
 
156
+ This hook can be used to monitor the state of each field and help fields configuration. It lets you:
157
+
158
+ 1. Capturing errors at the form/fieldset level, removing the need to setup invalid handler on each field.
159
+ 2. Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field using the [conform](#conform) helpers.
160
+
161
+ ```tsx
162
+ import { useForm, useFieldset } from '@conform-to/react';
163
+
164
+ /**
165
+ * Consider the schema as follow:
166
+ */
167
+ type Book = {
168
+ name: string;
169
+ isbn: string;
170
+ };
171
+
172
+ function BookFieldset() {
173
+ const formProps = useForm();
174
+ const { name, isbn } = useFieldset<Book>(
175
+ /**
176
+ * A ref object of the form element or fieldset element
177
+ */
178
+ formProps.ref,
179
+ {
180
+ /**
181
+ * The prefix used to generate the name of nested fields.
182
+ */
183
+ name: 'book',
184
+
185
+ /**
186
+ * An object representing the initial value of the fieldset.
187
+ */
188
+ defaultValue: {
189
+ isbn: '0340013818',
190
+ },
191
+
192
+ /**
193
+ * An object describing the initial error of each field
194
+ */
195
+ initialError: {
196
+ isbn: 'Invalid ISBN',
197
+ },
198
+
199
+ /**
200
+ * An object describing the constraint of each field
201
+ */
202
+ constraint: {
203
+ isbn: {
204
+ required: true,
205
+ pattern: '[0-9]{10,13}',
206
+ },
207
+ },
208
+
209
+ /**
210
+ * The id of the form. This is required only if you
211
+ * are connecting each field to a form remotely.
212
+ */
213
+ form: 'remote-form-id',
214
+ },
215
+ );
216
+
217
+ /**
218
+ * Latest error of the field
219
+ * This would be 'Invalid ISBN' initially as specified
220
+ * in the initialError config
221
+ */
222
+ console.log(book.error);
223
+
224
+ /**
225
+ * This would be `book.isbn` instead of `isbn`
226
+ * if the `name` option is provided
227
+ */
228
+ console.log(book.config.name);
229
+
230
+ /**
231
+ * This would be `0340013818` if specified
232
+ * on the `initalValue` option
233
+ */
234
+ console.log(book.config.defaultValue);
235
+
236
+ /**
237
+ * Initial error message
238
+ * This would be 'Invalid ISBN' if specified
239
+ */
240
+ console.log(book.config.initialError);
241
+
242
+ /**
243
+ * This would be `random-form-id`
244
+ * because of the `form` option provided
245
+ */
246
+ console.log(book.config.form);
247
+
248
+ /**
249
+ * Constraint of the field (required, minLength etc)
250
+ *
251
+ * For example, the constraint of the isbn field would be:
252
+ * {
253
+ * required: true,
254
+ * pattern: '[0-9]{10,13}'
255
+ * }
256
+ */
257
+ console.log(book.config.required);
258
+ console.log(book.config.pattern);
259
+
260
+ return <form {...formProps}>{/* ... */}</form>;
261
+ }
262
+ ```
263
+
264
+ If you don't have direct access to the form ref, you can also pass a fieldset ref.
265
+
266
+ ```tsx
267
+ import { useFieldset } from '@conform-to/react';
268
+ import { useRef } from 'react';
269
+
270
+ function Fieldset() {
271
+ const ref = useRef();
272
+ const fieldset = useFieldset(ref);
273
+
274
+ return <fieldset ref={ref}>{/* ... */}</fieldset>;
275
+ }
276
+ ```
277
+
278
+ <details>
279
+ <summary>Is it required to provide the FieldsetConfig to `useFieldset`?</summary>
280
+
281
+ No. The only thing required is the ref object. All the config is optional. You can always pass them to each fields manually.
282
+
283
+ ```tsx
284
+ import { useForm, useFieldset } from '@conform-to/react';
285
+
286
+ function SubscriptionForm() {
287
+ const formProps = useForm();
288
+ const { email } = useFieldset(formProps.ref);
289
+
290
+ return (
291
+ <form {...formProps}>
292
+ <input
293
+ type="email"
294
+ name={email.config.name}
295
+ defaultValue="support@conform.dev"
296
+ required
297
+ />
298
+ </form>
299
+ );
300
+ }
301
+ ```
302
+
303
+ </details>
304
+
305
+ <details>
306
+ <summary>Why does `useFieldset` require a ref object of the form or fieldset?</summary>
307
+
308
+ Unlike most of the form validation library out there, **conform** use the DOM as its context provider. As the dom maintains a link between each input / button / fieldset with the form through the [form property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#properties) of these elements. The ref object allows us restricting the scope to elements associated to the same form only.
309
+
310
+ ```tsx
311
+ function ExampleForm() {
312
+ const formRef = useRef();
313
+ const inputRef = useRef();
314
+
315
+ useEffect(() => {
316
+ // Both statements will log `true`
317
+ console.log(formRef.current === inputRef.current.form);
318
+ console.log(formRef.current.elements.namedItem('title') === inputRef.current)
319
+ }, []);
320
+
321
+ return (
322
+ <form ref={formRef}>
323
+ <input ref={inputRef} name="title">
324
+ </form>
325
+ );
326
+ }
327
+ ```
328
+
329
+ </details>
330
+
331
+ ---
332
+
21
333
  ### useFieldList
334
+
335
+ It returns a list of key and config, with a group of helpers configuring buttons for list manipulation
336
+
337
+ ```tsx
338
+ import { useFieldset, useFieldList } from '@conform-to/react';
339
+ import { useRef } from 'react';
340
+
341
+ /**
342
+ * Consider the schema as follow:
343
+ */
344
+ type Book = {
345
+ name: string;
346
+ isbn: string;
347
+ };
348
+
349
+ type Collection = {
350
+ books: Book[];
351
+ };
352
+
353
+ function CollectionFieldset() {
354
+ const ref = useRef();
355
+ const { books } = useFieldset<Collection>(ref);
356
+ const [bookList, control] = useFieldList(ref, books);
357
+
358
+ return (
359
+ <fieldset ref={ref}>
360
+ {bookList.map((book, index) => (
361
+ <div key={book.key}>
362
+ {/* To setup the fields */}
363
+ <input
364
+ name={`${book.config.name}.name`}
365
+ defaultValue={book.config.defaultValue.name}
366
+ />
367
+ <input
368
+ name={`${book.config.name}.isbn`}
369
+ defaultValue={book.config.defaultValue.isbn}
370
+ />
371
+
372
+ {/* To setup a delete button */}
373
+ <button {...control.remove({ index })}>Delete</button>
374
+ </div>
375
+ ))}
376
+
377
+ {/* To setup a button that can append a new row with optional default value */}
378
+ <button {...control.append({ defaultValue: { name: '', isbn: '' } })}>
379
+ add
380
+ </button>
381
+ </fieldset>
382
+ );
383
+ }
384
+ ```
385
+
386
+ This hook can also be used in combination with `useFieldset` to distribute the config:
387
+
388
+ ```tsx
389
+ import { useForm, useFieldset, useFieldList } from '@conform-to/react';
390
+ import { useRef } from 'react';
391
+
392
+ function CollectionFieldset() {
393
+ const ref = useRef();
394
+ const { books } = useFieldset<Collection>(ref);
395
+ const [bookList, control] = useFieldList(ref, books);
396
+
397
+ return (
398
+ <fieldset ref={ref}>
399
+ {bookList.map((book, index) => (
400
+ <div key={book.key}>
401
+ {/* `book.props` is a FieldConfig object similar to `books` */}
402
+ <BookFieldset {...book.config}>
403
+
404
+ {/* To setup a delete button */}
405
+ <button {...control.remove({ index })}>Delete</button>
406
+ </div>
407
+ ))}
408
+
409
+ {/* To setup a button that can append a new row */}
410
+ <button {...control.append()}>add</button>
411
+ </fieldset>
412
+ );
413
+ }
414
+
415
+ /**
416
+ * This is basically the BookFieldset component from
417
+ * the `useFieldset` example, but setting all the
418
+ * options with the component props instead
419
+ */
420
+ function BookFieldset({ name, form, defaultValue, error }) {
421
+ const ref = useRef();
422
+ const { name, isbn } = useFieldset(ref, {
423
+ name,
424
+ form,
425
+ defaultValue,
426
+ error,
427
+ });
428
+
429
+ return (
430
+ <fieldset ref={ref}>
431
+ {/* ... */}
432
+ </fieldset>
433
+ );
434
+ }
435
+ ```
436
+
437
+ <details>
438
+ <summary>What can I do with `controls`?</summary>
439
+
440
+ ```tsx
441
+ // To append a new row with optional defaultValue
442
+ <button {...controls.append({ defaultValue })}>Append</button>;
443
+
444
+ // To prepend a new row with optional defaultValue
445
+ <button {...controls.prepend({ defaultValue })}>Prepend</button>;
446
+
447
+ // To remove a row by index
448
+ <button {...controls.remove({ index })}>Remove</button>;
449
+
450
+ // To replace a row with another defaultValue
451
+ <button {...controls.replace({ index, defaultValue })}>Replace</button>;
452
+
453
+ // To reorder a particular row to an another index
454
+ <button {...controls.reorder({ from, to })}>Reorder</button>;
455
+ ```
456
+
457
+ </details>
458
+
459
+ ---
460
+
461
+ ### useControlledInput
462
+
463
+ It returns the properties required to configure a shadow input for validation. This is particular useful when integrating dropdown and datepicker whichs introduces custom input mode.
464
+
465
+ ```tsx
466
+ import { useFieldset, useControlledInput } from '@conform-to/react';
467
+ import { Select, MenuItem } from '@mui/material';
468
+ import { useRef } from 'react';
469
+
470
+ function MuiForm() {
471
+ const ref = useRef();
472
+ const { category } = useFieldset(schema);
473
+ const [inputProps, control] = useControlledInput(category);
474
+
475
+ return (
476
+ <fieldset ref={ref}>
477
+ {/* Render a shadow input somewhere */}
478
+ <input {...inputProps} />
479
+
480
+ {/* MUI Select is a controlled component */}
481
+ <Select
482
+ label="Category"
483
+ value={control.value}
484
+ onChange={control.onChange}
485
+ onBlur={control.onBlur}
486
+ inputProps={{
487
+ onInvalid: control.onInvalid
488
+ }}
489
+ >
490
+ <MenuItem value="">Please select</MenuItem>
491
+ <MenuItem value="a">Category A</MenuItem>
492
+ <MenuItem value="b">Category B</MenuItem>
493
+ <MenuItem value="c">Category C</MenuItem>
494
+ </TextField>
495
+ </fieldset>
496
+ )
497
+ }
498
+ ```
499
+
500
+ ---
501
+
502
+ ### conform
503
+
504
+ It provides several helpers to configure a native input field quickly:
505
+
506
+ ```tsx
507
+ import { useFieldset, conform } from '@conform-to/react';
508
+ import { useRef } from 'react';
509
+
510
+ function RandomForm() {
511
+ const ref = useRef();
512
+ const { cateogry } = useFieldset(ref);
513
+
514
+ return (
515
+ <fieldset ref={ref}>
516
+ <input {...conform.input(cateogry, { type: 'text' })} />
517
+ <textarea {...conform.textarea(cateogry)} />
518
+ <select {...conform.select(cateogry)}>{/* ... */}</select>
519
+ </fieldset>
520
+ );
521
+ }
522
+ ```
523
+
524
+ This is equivalent to:
525
+
526
+ ```tsx
527
+ function RandomForm() {
528
+ const ref = useRef();
529
+ const { cateogry } = useFieldset(ref);
530
+
531
+ return (
532
+ <fieldset ref={ref}>
533
+ <input
534
+ type="text"
535
+ name={cateogry.name}
536
+ form={cateogry.form}
537
+ defaultValue={cateogry.defaultValue}
538
+ requried={cateogry.required}
539
+ minLength={cateogry.minLength}
540
+ maxLength={cateogry.maxLength}
541
+ min={cateogry.min}
542
+ max={cateogry.max}
543
+ multiple={cateogry.multiple}
544
+ pattern={cateogry.pattern}
545
+ >
546
+ <textarea
547
+ name={cateogry.name}
548
+ form={cateogry.form}
549
+ defaultValue={cateogry.defaultValue}
550
+ requried={cateogry.required}
551
+ minLength={cateogry.minLength}
552
+ maxLength={cateogry.maxLength}
553
+ />
554
+ <select
555
+ name={cateogry.name}
556
+ form={cateogry.form}
557
+ defaultValue={cateogry.defaultValue}
558
+ requried={cateogry.required}
559
+ multiple={cateogry.multiple}
560
+ >
561
+ {/* ... */}
562
+ </select>
563
+ </fieldset>
564
+ );
565
+ }
566
+ ```
package/helpers.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type FieldConfig } from '@conform-to/dom';
1
+ import { type FieldConfig, type Primitive } from '@conform-to/dom';
2
2
  import { type InputHTMLAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes } from 'react';
3
- export declare function input<Type extends string | number | Date | undefined>(config: FieldConfig<Type>, { type, value }?: {
3
+ export declare function input<Schema extends Primitive>(config: FieldConfig<Schema>, { type, value }?: {
4
4
  type?: string;
5
5
  value?: string;
6
6
  }): InputHTMLAttributes<HTMLInputElement>;
7
- export declare function select<T extends any>(config: FieldConfig<T>): SelectHTMLAttributes<HTMLSelectElement>;
8
- export declare function textarea<T extends string | undefined>(config: FieldConfig<T>): TextareaHTMLAttributes<HTMLTextAreaElement>;
7
+ export declare function select<Schema extends Primitive | Array<Primitive>>(config: FieldConfig<Schema>): SelectHTMLAttributes<HTMLSelectElement>;
8
+ export declare function textarea<Schema extends Primitive>(config: FieldConfig<Schema>): TextareaHTMLAttributes<HTMLTextAreaElement>;
package/helpers.js CHANGED
@@ -3,50 +3,55 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  function input(config) {
6
- var _config$initialValue, _config$constraint, _config$constraint2, _config$constraint3, _config$constraint4, _config$constraint5, _config$constraint6, _config$constraint7;
7
-
8
6
  var {
9
7
  type,
10
8
  value
11
9
  } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
12
10
  var isCheckboxOrRadio = type === 'checkbox' || type === 'radio';
13
- return {
11
+ var attributes = {
14
12
  type,
15
13
  name: config.name,
16
14
  form: config.form,
17
- value: isCheckboxOrRadio ? value : undefined,
18
- defaultValue: !isCheckboxOrRadio ? "".concat((_config$initialValue = config.initialValue) !== null && _config$initialValue !== void 0 ? _config$initialValue : '') : undefined,
19
- defaultChecked: isCheckboxOrRadio ? config.initialValue === value : undefined,
20
- required: (_config$constraint = config.constraint) === null || _config$constraint === void 0 ? void 0 : _config$constraint.required,
21
- minLength: (_config$constraint2 = config.constraint) === null || _config$constraint2 === void 0 ? void 0 : _config$constraint2.minLength,
22
- maxLength: (_config$constraint3 = config.constraint) === null || _config$constraint3 === void 0 ? void 0 : _config$constraint3.maxLength,
23
- min: (_config$constraint4 = config.constraint) === null || _config$constraint4 === void 0 ? void 0 : _config$constraint4.min,
24
- max: (_config$constraint5 = config.constraint) === null || _config$constraint5 === void 0 ? void 0 : _config$constraint5.max,
25
- step: (_config$constraint6 = config.constraint) === null || _config$constraint6 === void 0 ? void 0 : _config$constraint6.step,
26
- pattern: (_config$constraint7 = config.constraint) === null || _config$constraint7 === void 0 ? void 0 : _config$constraint7.pattern
15
+ required: config.required,
16
+ minLength: config.minLength,
17
+ maxLength: config.maxLength,
18
+ min: config.min,
19
+ max: config.max,
20
+ step: config.step,
21
+ pattern: config.pattern,
22
+ multiple: config.multiple
27
23
  };
24
+
25
+ if (isCheckboxOrRadio) {
26
+ attributes.value = value !== null && value !== void 0 ? value : 'on';
27
+ attributes.defaultChecked = config.defaultValue === attributes.value;
28
+ } else {
29
+ attributes.defaultValue = config.defaultValue;
30
+ }
31
+
32
+ return attributes;
28
33
  }
29
34
  function select(config) {
30
- var _config$initialValue2, _config$constraint8, _config$constraint9;
35
+ var _config$defaultValue;
31
36
 
32
37
  return {
33
38
  name: config.name,
34
39
  form: config.form,
35
- defaultValue: "".concat((_config$initialValue2 = config.initialValue) !== null && _config$initialValue2 !== void 0 ? _config$initialValue2 : ''),
36
- required: (_config$constraint8 = config.constraint) === null || _config$constraint8 === void 0 ? void 0 : _config$constraint8.required,
37
- multiple: (_config$constraint9 = config.constraint) === null || _config$constraint9 === void 0 ? void 0 : _config$constraint9.multiple
40
+ defaultValue: config.multiple ? Array.isArray(config.defaultValue) ? config.defaultValue : [] : "".concat((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : ''),
41
+ required: config.required,
42
+ multiple: config.multiple
38
43
  };
39
44
  }
40
45
  function textarea(config) {
41
- var _config$initialValue3, _config$constraint10, _config$constraint11, _config$constraint12;
46
+ var _config$defaultValue2;
42
47
 
43
48
  return {
44
49
  name: config.name,
45
50
  form: config.form,
46
- defaultValue: "".concat((_config$initialValue3 = config.initialValue) !== null && _config$initialValue3 !== void 0 ? _config$initialValue3 : ''),
47
- required: (_config$constraint10 = config.constraint) === null || _config$constraint10 === void 0 ? void 0 : _config$constraint10.required,
48
- minLength: (_config$constraint11 = config.constraint) === null || _config$constraint11 === void 0 ? void 0 : _config$constraint11.minLength,
49
- maxLength: (_config$constraint12 = config.constraint) === null || _config$constraint12 === void 0 ? void 0 : _config$constraint12.maxLength
51
+ defaultValue: "".concat((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : ''),
52
+ required: config.required,
53
+ minLength: config.minLength,
54
+ maxLength: config.maxLength
50
55
  };
51
56
  }
52
57