@conform-to/react 0.5.0-pre.0 → 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,23 +236,21 @@ function ExampleForm() {
324
236
 
325
237
  ### useFieldList
326
238
 
327
- It returns a list of key, config and error, with helpers to configure [list command](/docs/submission.md#list-command) button.
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
247
  type Schema = {
337
- list: string[];
248
+ items: string[];
338
249
  };
339
250
 
340
- function CollectionFieldset() {
341
- const ref = useRef<HTMLFieldsetElement>(null);
342
- const fieldset = useFieldset<Collection>(ref);
343
- const [list, command] = useFieldList(ref, fieldset.list.config);
251
+ function Example() {
252
+ const [form, { items }] = useForm<Schema>();
253
+ const list = useFieldList(form.ref, items.config);
344
254
 
345
255
  return (
346
256
  <fieldset ref={ref}>
@@ -349,137 +259,61 @@ function CollectionFieldset() {
349
259
  {/* Setup an input per item */}
350
260
  <input {...conform.input(item.config)} />
351
261
 
352
- {/* Error of each book */}
262
+ {/* Error of each item */}
353
263
  <span>{item.error}</span>
354
264
 
355
- {/* To setup a delete button */}
356
- <button {...command.remove({ index })}>Delete</button>
357
- </div>
358
- ))}
359
-
360
- {/* To setup a button that can append a new row with optional default value */}
361
- <button {...command.append({ defaultValue: '' })}>add</button>
362
- </fieldset>
363
- );
364
- }
365
- ```
366
-
367
- This hook can also be used in combination with `useFieldset` for nested list:
368
-
369
- ```tsx
370
- import {
371
- type FieldConfig,
372
- useForm,
373
- useFieldset,
374
- useFieldList,
375
- } from '@conform-to/react';
376
- import { useRef } from 'react';
377
-
378
- /**
379
- * Consider the schema as follow:
380
- */
381
- type Schema = {
382
- list: Array<Item>;
383
- };
384
-
385
- type Item = {
386
- title: string;
387
- description: string;
388
- };
389
-
390
- function CollectionFieldset() {
391
- const ref = useRef<HTMLFieldsetElement>(null);
392
- const fieldset = useFieldset<Collection>(ref);
393
- const [list, command] = useFieldList(ref, fieldset.list.config);
394
-
395
- return (
396
- <fieldset ref={ref}>
397
- {list.map((item, index) => (
398
- <div key={item.key}>
399
- {/* Pass the item config to another fieldset*/}
400
- <ItemFieldset {...item.config} />
265
+ {/* Setup a delete button (Note: It is `items` not `item`) */}
266
+ <button {...list.remove(items.config.name, { index })}>Delete</button>
401
267
  </div>
402
268
  ))}
403
- </fieldset>
404
- );
405
- }
406
-
407
- function ItemFieldset(config: FieldConfig<Item>) {
408
- const ref = useRef<HTMLFieldsetElement>(null);
409
- const { title, description } = useFieldset(ref, config);
410
269
 
411
- return (
412
- <fieldset ref={ref}>
413
- <input {...conform.input(title.config)} />
414
- <span>{title.error}</span>
415
-
416
- <input {...conform.input(description.config)} />
417
- <span>{description.error}</span>
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>
418
274
  </fieldset>
419
275
  );
420
276
  }
421
277
  ```
422
278
 
423
- <details>
424
- <summary>What can I do with `command`?</summary>
425
-
426
- ```tsx
427
- // To append a new row with optional defaultValue
428
- <button {...command.append({ defaultValue })}>Append</button>;
429
-
430
- // To prepend a new row with optional defaultValue
431
- <button {...command.prepend({ defaultValue })}>Prepend</button>;
432
-
433
- // To remove a row by index
434
- <button {...command.remove({ index })}>Remove</button>;
435
-
436
- // To replace a row with another defaultValue
437
- <button {...command.replace({ index, defaultValue })}>Replace</button>;
438
-
439
- // To reorder a particular row to an another index
440
- <button {...command.reorder({ from, to })}>Reorder</button>;
441
- ```
442
-
443
- </details>
444
-
445
279
  ---
446
280
 
447
281
  ### useControlledInput
448
282
 
449
- 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.
450
284
 
451
285
  ```tsx
452
- import { useFieldset, useControlledInput } from '@conform-to/react';
286
+ import { useForm, useControlledInput } from '@conform-to/react';
453
287
  import { Select, MenuItem } from '@mui/material';
454
288
  import { useRef } from 'react';
455
289
 
456
290
  function MuiForm() {
457
- const ref = useRef();
458
- const { category } = useFieldset(schema);
291
+ const [form, { category }] = useForm();
459
292
  const [inputProps, control] = useControlledInput(category.config);
460
293
 
461
294
  return (
462
- <fieldset ref={ref}>
295
+ <form {...form.props}>
463
296
  {/* Render a shadow input somewhere */}
464
297
  <input {...inputProps} />
465
298
 
466
299
  {/* MUI Select is a controlled component */}
467
- <Select
300
+ <TextField
468
301
  label="Category"
469
302
  inputRef={control.ref}
470
303
  value={control.value}
471
304
  onChange={control.onChange}
472
305
  onBlur={control.onBlur}
473
306
  inputProps={{
474
- onInvalid: control.onInvalid
307
+ onInvalid: control.onInvalid,
475
308
  }}
309
+ select
476
310
  >
477
311
  <MenuItem value="">Please select</MenuItem>
478
312
  <MenuItem value="a">Category A</MenuItem>
479
313
  <MenuItem value="b">Category B</MenuItem>
480
314
  <MenuItem value="c">Category C</MenuItem>
481
315
  </TextField>
482
- </fieldset>
316
+ </form>
483
317
  );
484
318
  }
485
319
  ```
@@ -488,55 +322,40 @@ function MuiForm() {
488
322
 
489
323
  ### conform
490
324
 
491
- It provides several helpers to configure a native input field quickly:
492
-
493
- ```tsx
494
- import { useFieldset, conform } from '@conform-to/react';
495
- import { useRef } from 'react';
496
-
497
- function RandomForm() {
498
- const ref = useRef();
499
- const { category } = useFieldset(ref);
325
+ It provides several helpers to remove the boilerplate when configuring a form control.
500
326
 
501
- return (
502
- <fieldset ref={ref}>
503
- <input {...conform.input(category.config, { type: 'text' })} />
504
- <textarea {...conform.textarea(category.config)} />
505
- <select {...conform.select(category.config)}>{/* ... */}</select>
506
- </fieldset>
507
- );
508
- }
509
- ```
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).
510
328
 
511
- This is equivalent to:
329
+ Before:
512
330
 
513
331
  ```tsx
514
- function RandomForm() {
515
- const ref = useRef();
516
- const { category } = useFieldset(ref);
332
+ import { useForm } from '@conform-to/react';
333
+
334
+ function Example() {
335
+ const [form, { title, description, category }] = useForm();
517
336
 
518
337
  return (
519
- <fieldset ref={ref}>
338
+ <form {...form.props}>
520
339
  <input
521
340
  type="text"
522
- name={category.config.name}
523
- form={category.config.form}
524
- defaultValue={category.config.defaultValue}
525
- requried={category.config.required}
526
- minLength={category.config.minLength}
527
- maxLength={category.config.maxLength}
528
- min={category.config.min}
529
- max={category.config.max}
530
- multiple={category.config.multiple}
531
- pattern={category.config.pattern}
532
- >
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
+ />
533
352
  <textarea
534
- name={category.config.name}
535
- form={category.config.form}
536
- defaultValue={category.config.defaultValue}
537
- requried={category.config.required}
538
- minLength={category.config.minLength}
539
- 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}
540
359
  />
541
360
  <select
542
361
  name={category.config.name}
@@ -547,7 +366,118 @@ function RandomForm() {
547
366
  >
548
367
  {/* ... */}
549
368
  </select>
550
- </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>
551
481
  );
552
482
  }
553
483
  ```
@@ -556,13 +486,13 @@ function RandomForm() {
556
486
 
557
487
  ### getFormElements
558
488
 
559
- 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.
560
490
 
561
491
  ```tsx
562
492
  import { useForm, parse, getFormElements } from '@conform-to/react';
563
493
 
564
494
  export default function LoginForm() {
565
- const form = useForm({
495
+ const [form] = useForm({
566
496
  onValidate({ form, formData }) {
567
497
  const submission = parse(formData);
568
498