@conform-to/react 0.4.1 → 0.5.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
@@ -20,19 +20,25 @@
20
20
 
21
21
  ### useForm
22
22
 
23
- 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.
23
+ By default, the browser calls the [reportValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity) API on the form element when a submission is triggered. This checks the validity of all the fields and reports through the error bubbles.
24
24
 
25
- This hook enhances the form validation behaviour in 3 parts:
25
+ This hook enhances the form validation behaviour by:
26
26
 
27
- 1. It enhances form validation with custom rules by subscribing to different DOM events and reporting the errors only when it is configured to do so.
28
- 2. It unifies client and server validation in one place.
29
- 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.
27
+ - Enabling customizing form validation behaviour.
28
+ - Capturing the error message and removes the error bubbles.
29
+ - Preparing all properties required to configure the dom elements.
30
30
 
31
31
  ```tsx
32
32
  import { useForm } from '@conform-to/react';
33
33
 
34
34
  function LoginForm() {
35
- const form = useForm({
35
+ const [form, { email, password }] = useForm({
36
+ /**
37
+ * If the form id is provided, Id for label,
38
+ * input and error elements will be derived.
39
+ */
40
+ id: undefined,
41
+
36
42
  /**
37
43
  * Validation mode.
38
44
  * Support "client-only" or "server-validation".
@@ -59,6 +65,11 @@ function LoginForm() {
59
65
  */
60
66
  state: undefined;
61
67
 
68
+ /**
69
+ * An object describing the constraint of each field
70
+ */
71
+ constraint: undefined;
72
+
62
73
  /**
63
74
  * Enable native validation before hydation.
64
75
  *
@@ -77,14 +88,14 @@ function LoginForm() {
77
88
  * A function to be called when the form should be (re)validated.
78
89
  * Only sync validation is supported
79
90
  */
80
- onValidate({ form, formData, submission }) {
91
+ onValidate({ form, formData }) {
81
92
  // ...
82
93
  },
83
94
 
84
95
  /**
85
96
  * The submit event handler of the form.
86
97
  */
87
- onSubmit(event, { form, formData, submission }) {
98
+ onSubmit(event, { formData, submission }) {
88
99
  // ...
89
100
  },
90
101
  });
@@ -96,15 +107,16 @@ function LoginForm() {
96
107
  <details>
97
108
  <summary>What is `form.props`?</summary>
98
109
 
99
- It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below:
110
+ It is a group of properties required to hook into form events. They can also be set explicitly as shown below:
100
111
 
101
112
  ```tsx
102
113
  function RandomForm() {
103
- const form = useForm();
114
+ const [form] = useForm();
104
115
 
105
116
  return (
106
117
  <form
107
118
  ref={form.props.ref}
119
+ id={form.props.id}
108
120
  onSubmit={form.props.onSubmit}
109
121
  noValidate={form.props.noValidate}
110
122
  >
@@ -126,7 +138,7 @@ import { useFrom } from '@conform-to/react';
126
138
  import { Form } from '@remix-run/react';
127
139
 
128
140
  function LoginForm() {
129
- const form = useForm();
141
+ const [form] = useForm();
130
142
 
131
143
  return (
132
144
  <Form method="post" action="/login" {...form.props}>
@@ -138,157 +150,57 @@ function LoginForm() {
138
150
 
139
151
  </details>
140
152
 
141
- <details>
142
- <summary>Is the `onValidate` function required?</summary>
143
-
144
- The `onValidate` 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.
145
-
146
- ```tsx
147
- import { useForm, useFieldset } from '@conform-to/react';
148
-
149
- function LoginForm() {
150
- const formProps = useForm();
151
- const { email, password } = useFieldset(formProps.ref);
152
-
153
- return (
154
- <form {...formProps}>
155
- <label>
156
- <input type="email" name="email" required />
157
- {email.error}
158
- </label>
159
- <label>
160
- <input type="password" name="password" required />
161
- {password.error}
162
- </label>
163
- <button type="submit">Login</button>
164
- </form>
165
- );
166
- }
167
- ```
168
-
169
- </details>
170
-
171
153
  ---
172
154
 
173
155
  ### useFieldset
174
156
 
175
- This hook can be used to monitor the state of each field and help configuration. It lets you:
176
-
177
- 1. Capturing errors at the form/fieldset level, removing the need to setup invalid handler on each field.
178
- 2. Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field with the [conform](#conform) helpers.
157
+ This hook enables you to work with [nested object](/docs/configuration.md#nested-object) by monitoring the state of each nested field and prepraing the config required.
179
158
 
180
159
  ```tsx
181
- import { useForm, useFieldset } from '@conform-to/react';
160
+ import { useForm, useFieldset, conform } from '@conform-to/react';
182
161
 
183
- /**
184
- * Consider the schema as follow:
185
- */
186
- type Book = {
187
- name: string;
188
- isbn: string;
189
- };
162
+ interface Address {
163
+ street: string;
164
+ zipcode: string;
165
+ city: string;
166
+ country: string;
167
+ }
190
168
 
191
- function BookFieldset() {
192
- const formProps = useForm();
193
- const { name, isbn } = useFieldset<Book>(
194
- /**
195
- * A ref object of the form element or fieldset element
196
- */
197
- formProps.ref,
198
- {
199
- /**
200
- * The prefix used to generate the name of nested fields.
201
- */
202
- name: 'book',
203
-
204
- /**
205
- * An object representing the initial value of the fieldset.
206
- */
207
- defaultValue: {
208
- isbn: '0340013818',
209
- },
210
-
211
- /**
212
- * An object describing the initial error of each field
213
- */
214
- initialError: {
215
- isbn: 'Invalid ISBN',
216
- },
217
-
218
- /**
219
- * An object describing the constraint of each field
220
- */
221
- constraint: {
222
- isbn: {
223
- required: true,
224
- pattern: '[0-9]{10,13}',
225
- },
226
- },
227
-
228
- /**
229
- * The id of the form. This is required only if you
230
- * are connecting each field to a form remotely.
231
- */
232
- form: 'remote-form-id',
233
- },
169
+ function Example() {
170
+ const [form, { address }] = useForm<{ address: Address }>();
171
+ const { city, zipcode, street, country } = useFieldset(
172
+ form.ref,
173
+ address.config,
234
174
  );
235
175
 
236
- /**
237
- * Latest error of the field
238
- * This would be 'Invalid ISBN' initially as specified
239
- * in the initialError config
240
- */
241
- console.log(isbn.error);
242
-
243
- /**
244
- * This would be `book.isbn` instead of `isbn`
245
- * if the `name` option is provided
246
- */
247
- console.log(isbn.config.name);
248
-
249
- /**
250
- * This would be `0340013818` if specified
251
- * on the `initalValue` option
252
- */
253
- console.log(isbn.config.defaultValue);
254
-
255
- /**
256
- * Initial error message
257
- * This would be 'Invalid ISBN' if specified
258
- */
259
- console.log(isbn.config.initialError);
260
-
261
- /**
262
- * This would be `random-form-id`
263
- * because of the `form` option provided
264
- */
265
- console.log(isbn.config.form);
266
-
267
- /**
268
- * Constraint of the field (required, minLength etc)
269
- *
270
- * For example, the constraint of the isbn field would be:
271
- * {
272
- * required: true,
273
- * pattern: '[0-9]{10,13}'
274
- * }
275
- */
276
- console.log(isbn.config.required);
277
- console.log(isbn.config.pattern);
278
-
279
- return <form {...formProps}>{/* ... */}</form>;
176
+ return (
177
+ <form {...form.props}>
178
+ <fieldset>
179
+ <legned>Address</legend>
180
+ <input {...conform.input(street.config)} />
181
+ <div>{street.error}</div>
182
+ <input {...conform.input(zipcode.config)} />
183
+ <div>{zipcode.error}</div>
184
+ <input {...conform.input(city.config)} />
185
+ <div>{city.error}</div>
186
+ <input {...conform.input(country.config)} />
187
+ <div>{country.error}</div>
188
+ </fieldset>
189
+ <button>Submit</button>
190
+ </form>
191
+ );
280
192
  }
281
193
  ```
282
194
 
283
195
  If you don't have direct access to the form ref, you can also pass a fieldset ref.
284
196
 
285
197
  ```tsx
286
- import { useFieldset } from '@conform-to/react';
198
+ import { type FieldConfig, useFieldset } from '@conform-to/react';
287
199
  import { useRef } from 'react';
288
200
 
289
- function Fieldset() {
290
- const ref = useRef();
291
- const fieldset = useFieldset(ref);
201
+ function Fieldset(config: FieldConfig<Address>) {
202
+ const ref = useRef<HTMLFieldsetElement>(null);
203
+ const { city, zipcode, street, country } = useFieldset(ref, config);
292
204
 
293
205
  return <fieldset ref={ref}>{/* ... */}</fieldset>;
294
206
  }
@@ -297,7 +209,7 @@ function Fieldset() {
297
209
  <details>
298
210
  <summary>Why does `useFieldset` require a ref object of the form or fieldset?</summary>
299
211
 
300
- 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.
212
+ **conform** utilises the DOM as its context provider / input registry, which 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). The ref object allows it to restrict the scope to elements associated to the same form only.
301
213
 
302
214
  ```tsx
303
215
  function ExampleForm() {
@@ -324,164 +236,84 @@ function ExampleForm() {
324
236
 
325
237
  ### useFieldList
326
238
 
327
- It returns a list of key and config, with helpers to configure command buttons with [list command](/docs/submission.md#list-command).
239
+ This hook enables you to work with [array](/docs/configuration.md#array) and support [list](#list) command button builder to modify a list. It can also be used with [useFieldset](#usefieldset) for [nested list](/docs/configuration.md#nested-list) at the same time.
328
240
 
329
241
  ```tsx
330
- import { useFieldset, useFieldList } from '@conform-to/react';
331
- import { useRef } from 'react';
242
+ import { useForm, useFieldList, list } from '@conform-to/react';
332
243
 
333
244
  /**
334
245
  * Consider the schema as follow:
335
246
  */
336
- type Book = {
337
- name: string;
338
- isbn: string;
247
+ type Schema = {
248
+ items: string[];
339
249
  };
340
250
 
341
- type Collection = {
342
- books: Book[];
343
- };
344
-
345
- function CollectionFieldset() {
346
- const ref = useRef();
347
- const { books } = useFieldset<Collection>(ref);
348
- const [bookList, command] = useFieldList(ref, books.config);
251
+ function Example() {
252
+ const [form, { items }] = useForm<Schema>();
253
+ const list = useFieldList(form.ref, items.config);
349
254
 
350
255
  return (
351
256
  <fieldset ref={ref}>
352
- {bookList.map((book, index) => (
353
- <div key={book.key}>
354
- {/* To setup the fields */}
355
- <input
356
- name={`${book.config.name}.name`}
357
- defaultValue={book.config.defaultValue.name}
358
- />
359
- <input
360
- name={`${book.config.name}.isbn`}
361
- defaultValue={book.config.defaultValue.isbn}
362
- />
363
-
364
- {/* To setup a delete button */}
365
- <button {...command.remove({ index })}>Delete</button>
366
- </div>
367
- ))}
257
+ {list.map((item, index) => (
258
+ <div key={item.key}>
259
+ {/* Setup an input per item */}
260
+ <input {...conform.input(item.config)} />
368
261
 
369
- {/* To setup a button that can append a new row with optional default value */}
370
- <button {...command.append({ defaultValue: { name: '', isbn: '' } })}>
371
- add
372
- </button>
373
- </fieldset>
374
- );
375
- }
376
- ```
377
-
378
- This hook can also be used in combination with `useFieldset` to distribute the config:
379
-
380
- ```tsx
381
- import { useForm, useFieldset, useFieldList } from '@conform-to/react';
382
- import { useRef } from 'react';
383
-
384
- function CollectionFieldset() {
385
- const ref = useRef();
386
- const { books } = useFieldset<Collection>(ref);
387
- const [bookList, command] = useFieldList(ref, books.config);
262
+ {/* Error of each item */}
263
+ <span>{item.error}</span>
388
264
 
389
- return (
390
- <fieldset ref={ref}>
391
- {bookList.map((book, index) => (
392
- <div key={book.key}>
393
- {/* `book.config` is a FieldConfig object similar to `books` */}
394
- <BookFieldset {...book.config} />
395
-
396
- {/* To setup a delete button */}
397
- <button {...command.remove({ index })}>Delete</button>
265
+ {/* Setup a delete button (Note: It is `items` not `item`) */}
266
+ <button {...list.remove(items.config.name, { index })}>Delete</button>
398
267
  </div>
399
268
  ))}
400
269
 
401
- {/* To setup a button that can append a new row */}
402
- <button {...command.append()}>add</button>
270
+ {/* Setup a button that can append a new row with optional default value */}
271
+ <button {...list.append(items.config.name, { defaultValue: '' })}>
272
+ add
273
+ </button>
403
274
  </fieldset>
404
275
  );
405
276
  }
406
-
407
- /**
408
- * This is basically the BookFieldset component from
409
- * the `useFieldset` example, but setting all the
410
- * options with the component props instead
411
- */
412
- function BookFieldset({ name, form, defaultValue, error }) {
413
- const ref = useRef();
414
- const { name, isbn } = useFieldset(ref, {
415
- name,
416
- form,
417
- defaultValue,
418
- error,
419
- });
420
-
421
- return <fieldset ref={ref}>{/* ... */}</fieldset>;
422
- }
423
277
  ```
424
278
 
425
- <details>
426
- <summary>What can I do with `controls`?</summary>
427
-
428
- ```tsx
429
- // To append a new row with optional defaultValue
430
- <button {...controls.append({ defaultValue })}>Append</button>;
431
-
432
- // To prepend a new row with optional defaultValue
433
- <button {...controls.prepend({ defaultValue })}>Prepend</button>;
434
-
435
- // To remove a row by index
436
- <button {...controls.remove({ index })}>Remove</button>;
437
-
438
- // To replace a row with another defaultValue
439
- <button {...controls.replace({ index, defaultValue })}>Replace</button>;
440
-
441
- // To reorder a particular row to an another index
442
- <button {...controls.reorder({ from, to })}>Reorder</button>;
443
- ```
444
-
445
- </details>
446
-
447
279
  ---
448
280
 
449
281
  ### useControlledInput
450
282
 
451
- 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.
283
+ It returns the properties required to configure a shadow input for validation and helper to integrate it. This is particularly useful when [integrating custom input components](/docs/integrations.md#custom-input-component) like dropdown and datepicker.
452
284
 
453
285
  ```tsx
454
- import { useFieldset, useControlledInput } from '@conform-to/react';
286
+ import { useForm, useControlledInput } from '@conform-to/react';
455
287
  import { Select, MenuItem } from '@mui/material';
456
288
  import { useRef } from 'react';
457
289
 
458
290
  function MuiForm() {
459
- const ref = useRef();
460
- const { category } = useFieldset(schema);
291
+ const [form, { category }] = useForm();
461
292
  const [inputProps, control] = useControlledInput(category.config);
462
293
 
463
294
  return (
464
- <fieldset ref={ref}>
295
+ <form {...form.props}>
465
296
  {/* Render a shadow input somewhere */}
466
297
  <input {...inputProps} />
467
298
 
468
299
  {/* MUI Select is a controlled component */}
469
- <Select
300
+ <TextField
470
301
  label="Category"
471
302
  inputRef={control.ref}
472
303
  value={control.value}
473
304
  onChange={control.onChange}
474
305
  onBlur={control.onBlur}
475
306
  inputProps={{
476
- onInvalid: control.onInvalid
307
+ onInvalid: control.onInvalid,
477
308
  }}
309
+ select
478
310
  >
479
311
  <MenuItem value="">Please select</MenuItem>
480
312
  <MenuItem value="a">Category A</MenuItem>
481
313
  <MenuItem value="b">Category B</MenuItem>
482
314
  <MenuItem value="c">Category C</MenuItem>
483
315
  </TextField>
484
- </fieldset>
316
+ </form>
485
317
  );
486
318
  }
487
319
  ```
@@ -490,55 +322,40 @@ function MuiForm() {
490
322
 
491
323
  ### conform
492
324
 
493
- It provides several helpers to configure a native input field quickly:
494
-
495
- ```tsx
496
- import { useFieldset, conform } from '@conform-to/react';
497
- import { useRef } from 'react';
498
-
499
- function RandomForm() {
500
- const ref = useRef();
501
- const { category } = useFieldset(ref);
325
+ It provides several helpers to remove the boilerplate when configuring a form control.
502
326
 
503
- return (
504
- <fieldset ref={ref}>
505
- <input {...conform.input(category.config, { type: 'text' })} />
506
- <textarea {...conform.textarea(category.config)} />
507
- <select {...conform.select(category.config)}>{/* ... */}</select>
508
- </fieldset>
509
- );
510
- }
511
- ```
327
+ You are recommended to create a wrapper on top if you need to integrate with custom input component. As the helper derives attributes for [accessibility](/docs/accessibility.md#configuration) concerns and helps [focus management](/docs/focus-management.md#focusing-before-javascript-is-loaded).
512
328
 
513
- This is equivalent to:
329
+ Before:
514
330
 
515
331
  ```tsx
516
- function RandomForm() {
517
- const ref = useRef();
518
- const { category } = useFieldset(ref);
332
+ import { useForm } from '@conform-to/react';
333
+
334
+ function Example() {
335
+ const [form, { title, description, category }] = useForm();
519
336
 
520
337
  return (
521
- <fieldset ref={ref}>
338
+ <form {...form.props}>
522
339
  <input
523
340
  type="text"
524
- name={category.config.name}
525
- form={category.config.form}
526
- defaultValue={category.config.defaultValue}
527
- requried={category.config.required}
528
- minLength={category.config.minLength}
529
- maxLength={category.config.maxLength}
530
- min={category.config.min}
531
- max={category.config.max}
532
- multiple={category.config.multiple}
533
- pattern={category.config.pattern}
534
- >
341
+ name={title.config.name}
342
+ form={title.config.form}
343
+ defaultValue={title.config.defaultValue}
344
+ requried={title.config.required}
345
+ minLength={title.config.minLength}
346
+ maxLength={title.config.maxLength}
347
+ min={title.config.min}
348
+ max={title.config.max}
349
+ multiple={title.config.multiple}
350
+ pattern={title.config.pattern}
351
+ />
535
352
  <textarea
536
- name={category.config.name}
537
- form={category.config.form}
538
- defaultValue={category.config.defaultValue}
539
- requried={category.config.required}
540
- minLength={category.config.minLength}
541
- maxLength={category.config.maxLength}
353
+ name={description.config.name}
354
+ form={description.config.form}
355
+ defaultValue={description.config.defaultValue}
356
+ requried={description.config.required}
357
+ minLength={description.config.minLength}
358
+ maxLength={description.config.maxLength}
542
359
  />
543
360
  <select
544
361
  name={category.config.name}
@@ -549,7 +366,118 @@ function RandomForm() {
549
366
  >
550
367
  {/* ... */}
551
368
  </select>
552
- </fieldset>
369
+ </form>
370
+ );
371
+ }
372
+ ```
373
+
374
+ After:
375
+
376
+ ```tsx
377
+ import { useForm, conform } from '@conform-to/react';
378
+
379
+ function Example() {
380
+ const [form, { title, description, category }] = useForm();
381
+
382
+ return (
383
+ <form {...form.props}>
384
+ <input {...conform.input(title.config, { type: 'text' })} />
385
+ <textarea {...conform.textarea(description.config)} />
386
+ <select {...conform.select(category.config)}>{/* ... */}</select>
387
+ </form>
388
+ );
389
+ }
390
+ ```
391
+
392
+ ---
393
+
394
+ ### list
395
+
396
+ It provides serveral helpers to configure a command button for [modifying a list](/docs/commands.md#modifying-a-list).
397
+
398
+ ```tsx
399
+ import { list } from '@conform-to/react';
400
+
401
+ function Example() {
402
+ return (
403
+ <form>
404
+ {/* To append a new row with optional defaultValue */}
405
+ <button {...list.append('name', { defaultValue })}>Append</button>
406
+
407
+ {/* To prepend a new row with optional defaultValue */}
408
+ <button {...list.prepend('name', { defaultValue })}>Prepend</button>
409
+
410
+ {/* To remove a row by index */}
411
+ <button {...list.remove('name', { index })}>Remove</button>
412
+
413
+ {/* To replace a row with another defaultValue */}
414
+ <button {...list.replace('name', { index, defaultValue })}>
415
+ Replace
416
+ </button>
417
+
418
+ {/* To reorder a particular row to an another index */}
419
+ <button {...list.reorder('name', { from, to })}>Reorder</button>
420
+ </form>
421
+ );
422
+ }
423
+ ```
424
+
425
+ ---
426
+
427
+ ### validate
428
+
429
+ It returns the properties required to configure a command button for [validation](/docs/commands.md#validation).
430
+
431
+ ```tsx
432
+ import { validate } from '@conform-to/react';
433
+
434
+ function Example() {
435
+ return (
436
+ <form>
437
+ {/* To validate a single field by name */}
438
+ <button {...validate('email')}>Validate email</button>
439
+
440
+ {/* To validate the whole form */}
441
+ <button {...validate()}>Validate</button>
442
+ </form>
443
+ );
444
+ }
445
+ ```
446
+
447
+ ---
448
+
449
+ ### requestCommand
450
+
451
+ It lets you [trigger a command](/docs/commands.md#triggering-a-command) without requiring users to click on a button. It supports both [list](#list) and [validate](#validate) command.
452
+
453
+ ```tsx
454
+ import {
455
+ useForm,
456
+ useFieldList,
457
+ conform,
458
+ list,
459
+ requestCommand,
460
+ } from '@conform-to/react';
461
+ import DragAndDrop from 'awesome-dnd-example';
462
+
463
+ export default function Todos() {
464
+ const [form, { tasks }] = useForm();
465
+ const taskList = useFieldList(form.ref, tasks.config);
466
+
467
+ const handleDrop = (from, to) =>
468
+ requestCommand(form.ref.current, list.reorder({ from, to }));
469
+
470
+ return (
471
+ <form {...form.props}>
472
+ <DragAndDrop onDrop={handleDrop}>
473
+ {taskList.map((task, index) => (
474
+ <div key={task.key}>
475
+ <input {...conform.input(task.config)} />
476
+ </div>
477
+ ))}
478
+ </DragAndDrop>
479
+ <button>Save</button>
480
+ </form>
553
481
  );
554
482
  }
555
483
  ```
@@ -558,13 +486,13 @@ function RandomForm() {
558
486
 
559
487
  ### getFormElements
560
488
 
561
- It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field.
489
+ It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field manually.
562
490
 
563
491
  ```tsx
564
492
  import { useForm, parse, getFormElements } from '@conform-to/react';
565
493
 
566
494
  export default function LoginForm() {
567
- const form = useForm({
495
+ const [form] = useForm({
568
496
  onValidate({ form, formData }) {
569
497
  const submission = parse(formData);
570
498