@conform-to/react 0.6.0-pre.0 → 0.6.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/README.md CHANGED
@@ -11,13 +11,12 @@
11
11
  - [useFieldList](#usefieldlist)
12
12
  - [useInputEvent](#useinputevent)
13
13
  - [conform](#conform)
14
+ - [parse](#parse)
15
+ - [validateConstraint](#validateconstraint)
14
16
  - [list](#list)
15
17
  - [validate](#validate)
16
18
  - [requestIntent](#requestintent)
17
- - [getFormElements](#getformelements)
18
- - [hasError](#haserror)
19
- - [parse](#parse)
20
- - [shouldValidate](#shouldvalidate)
19
+ - [isFieldElement](#isfieldelement)
21
20
 
22
21
  <!-- /aside -->
23
22
 
@@ -27,9 +26,9 @@ By default, the browser calls the [reportValidity()](https://developer.mozilla.o
27
26
 
28
27
  This hook enhances the form validation behaviour by:
29
28
 
30
- - Enabling customizing form validation behaviour.
31
- - Capturing the error message and removes the error bubbles.
32
- - Preparing all properties required to configure the dom elements.
29
+ - Enabling customizing validation logic.
30
+ - Capturing error message and removes the error bubbles.
31
+ - Preparing all properties required to configure the form elements.
33
32
 
34
33
  ```tsx
35
34
  import { useForm } from '@conform-to/react';
@@ -43,27 +42,35 @@ function LoginForm() {
43
42
  id: undefined,
44
43
 
45
44
  /**
46
- * Define when the error should be reported initially.
45
+ * Define when conform should start validation.
47
46
  * Support "onSubmit", "onChange", "onBlur".
48
47
  *
49
48
  * Default to `onSubmit`.
50
49
  */
51
- initialReport: 'onBlur',
50
+ shouldValidate: 'onSubmit',
51
+
52
+ /**
53
+ * Define when conform should revalidate again.
54
+ * Support "onSubmit", "onChange", "onBlur".
55
+ *
56
+ * Default to `onInput`.
57
+ */
58
+ shouldRevalidate: 'onInput',
52
59
 
53
60
  /**
54
61
  * An object representing the initial value of the form.
55
62
  */
56
- defaultValue: undefined;
63
+ defaultValue: undefined,
57
64
 
58
65
  /**
59
- * An object describing the state from the last submission
66
+ * The last submission result from the server
60
67
  */
61
- state: undefined;
68
+ lastSubmission: undefined,
62
69
 
63
70
  /**
64
71
  * An object describing the constraint of each field
65
72
  */
66
- constraint: undefined;
73
+ constraint: undefined,
67
74
 
68
75
  /**
69
76
  * Enable native validation before hydation.
@@ -90,7 +97,7 @@ function LoginForm() {
90
97
  /**
91
98
  * The submit event handler of the form.
92
99
  */
93
- onSubmit(event, { formData, submission }) {
100
+ onSubmit(event, { formData, submission, action, encType, method }) {
94
101
  // ...
95
102
  },
96
103
  });
@@ -165,20 +172,20 @@ function Example() {
165
172
  const [form, { address }] = useForm<{ address: Address }>();
166
173
  const { city, zipcode, street, country } = useFieldset(
167
174
  form.ref,
168
- address.config,
175
+ address,
169
176
  );
170
177
 
171
178
  return (
172
179
  <form {...form.props}>
173
180
  <fieldset>
174
181
  <legned>Address</legend>
175
- <input {...conform.input(street.config)} />
182
+ <input {...conform.input(street)} />
176
183
  <div>{street.error}</div>
177
- <input {...conform.input(zipcode.config)} />
184
+ <input {...conform.input(zipcode)} />
178
185
  <div>{zipcode.error}</div>
179
- <input {...conform.input(city.config)} />
186
+ <input {...conform.input(city)} />
180
187
  <div>{city.error}</div>
181
- <input {...conform.input(country.config)} />
188
+ <input {...conform.input(country)} />
182
189
  <div>{country.error}</div>
183
190
  </fieldset>
184
191
  <button>Submit</button>
@@ -204,7 +211,7 @@ function Fieldset(config: FieldConfig<Address>) {
204
211
  <details>
205
212
  <summary>Why does `useFieldset` require a ref object of the form or fieldset?</summary>
206
213
 
207
- **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.
214
+ **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 form elements associated to the same form only.
208
215
 
209
216
  ```tsx
210
217
  function ExampleForm() {
@@ -231,7 +238,7 @@ function ExampleForm() {
231
238
 
232
239
  ### useFieldList
233
240
 
234
- 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.
241
+ This hook enables you to work with [array](/docs/configuration.md#array) and support the [list](#list) intent 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.
235
242
 
236
243
  ```tsx
237
244
  import { useForm, useFieldList, list } from '@conform-to/react';
@@ -245,27 +252,25 @@ type Schema = {
245
252
 
246
253
  function Example() {
247
254
  const [form, { items }] = useForm<Schema>();
248
- const list = useFieldList(form.ref, items.config);
255
+ const itemsList = useFieldList(form.ref, items);
249
256
 
250
257
  return (
251
258
  <fieldset ref={ref}>
252
- {list.map((item, index) => (
259
+ {itemsList.map((item, index) => (
253
260
  <div key={item.key}>
254
261
  {/* Setup an input per item */}
255
- <input {...conform.input(item.config)} />
262
+ <input {...conform.input(item)} />
256
263
 
257
264
  {/* Error of each item */}
258
265
  <span>{item.error}</span>
259
266
 
260
267
  {/* Setup a delete button (Note: It is `items` not `item`) */}
261
- <button {...list.remove(items.config.name, { index })}>Delete</button>
268
+ <button {...list.remove(items.name, { index })}>Delete</button>
262
269
  </div>
263
270
  ))}
264
271
 
265
272
  {/* Setup a button that can append a new row with optional default value */}
266
- <button {...list.append(items.config.name, { defaultValue: '' })}>
267
- add
268
- </button>
273
+ <button {...list.append(items.name, { defaultValue: '' })}>add</button>
269
274
  </fieldset>
270
275
  );
271
276
  }
@@ -284,9 +289,9 @@ import { useState, useRef } from 'react';
284
289
 
285
290
  function MuiForm() {
286
291
  const [form, { category }] = useForm();
287
- const [value, setValue] = useState(category.config.defaultValue ?? '');
292
+ const [value, setValue] = useState(category.defaultValue ?? '');
288
293
  const [ref, control] = useInputEvent({
289
- onReset: () => setValue(category.config.defaultValue ?? ''),
294
+ onReset: () => setValue(category.defaultValue ?? ''),
290
295
  });
291
296
  const inputRef = useRef<HTMLInputElement>(null);
292
297
 
@@ -295,7 +300,7 @@ function MuiForm() {
295
300
  {/* Render a shadow input somewhere */}
296
301
  <input
297
302
  ref={ref}
298
- {...conform.input(category.config, { hidden: true })}
303
+ {...conform.input(category, { hidden: true })}
299
304
  onChange={(e) => setValue(e.target.value)}
300
305
  onFocus={() => inputRef.current?.focus()}
301
306
  />
@@ -323,9 +328,9 @@ function MuiForm() {
323
328
 
324
329
  ### conform
325
330
 
326
- It provides several helpers to remove the boilerplate when configuring a form control.
331
+ It provides several helpers to remove the boilerplate when configuring a form control and derives attributes for [accessibility](/docs/accessibility.md#configuration) concerns and helps [focus management](/docs/focus-management.md#focusing-before-javascript-is-loaded).
327
332
 
328
- 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).
333
+ You can also create a wrapper on top if you need to integrate with custom input component.
329
334
 
330
335
  Before:
331
336
 
@@ -339,31 +344,31 @@ function Example() {
339
344
  <form {...form.props}>
340
345
  <input
341
346
  type="text"
342
- name={title.config.name}
343
- form={title.config.form}
344
- defaultValue={title.config.defaultValue}
345
- requried={title.config.required}
346
- minLength={title.config.minLength}
347
- maxLength={title.config.maxLength}
348
- min={title.config.min}
349
- max={title.config.max}
350
- multiple={title.config.multiple}
351
- pattern={title.config.pattern}
347
+ name={title.name}
348
+ form={title.form}
349
+ defaultValue={title.defaultValue}
350
+ requried={title.required}
351
+ minLength={title.minLength}
352
+ maxLength={title.maxLength}
353
+ min={title.min}
354
+ max={title.max}
355
+ multiple={title.multiple}
356
+ pattern={title.pattern}
352
357
  />
353
358
  <textarea
354
- name={description.config.name}
355
- form={description.config.form}
356
- defaultValue={description.config.defaultValue}
357
- requried={description.config.required}
358
- minLength={description.config.minLength}
359
- maxLength={description.config.maxLength}
359
+ name={description.name}
360
+ form={description.form}
361
+ defaultValue={description.defaultValue}
362
+ requried={description.required}
363
+ minLength={description.minLength}
364
+ maxLength={description.maxLength}
360
365
  />
361
366
  <select
362
- name={category.config.name}
363
- form={category.config.form}
364
- defaultValue={category.config.defaultValue}
365
- requried={category.config.required}
366
- multiple={category.config.multiple}
367
+ name={category.name}
368
+ form={category.form}
369
+ defaultValue={category.defaultValue}
370
+ requried={category.required}
371
+ multiple={category.multiple}
367
372
  >
368
373
  {/* ... */}
369
374
  </select>
@@ -382,9 +387,9 @@ function Example() {
382
387
 
383
388
  return (
384
389
  <form {...form.props}>
385
- <input {...conform.input(title.config, { type: 'text' })} />
386
- <textarea {...conform.textarea(description.config)} />
387
- <select {...conform.select(category.config)}>{/* ... */}</select>
390
+ <input {...conform.input(title, { type: 'text' })} />
391
+ <textarea {...conform.textarea(description)} />
392
+ <select {...conform.select(category)}>{/* ... */}</select>
388
393
  </form>
389
394
  );
390
395
  }
@@ -392,9 +397,117 @@ function Example() {
392
397
 
393
398
  ---
394
399
 
400
+ ### parse
401
+
402
+ It parses the formData based on the [naming convention](/docs/configuration.md#naming-convention) with the validation result from the resolver.
403
+
404
+ ```tsx
405
+ import { parse } from '@conform-to/react';
406
+
407
+ const formData = new FormData();
408
+ const submission = parse(formData, {
409
+ resolve({ email, password }) {
410
+ const error: Record<string, string> = {};
411
+
412
+ if (typeof email !== 'string') {
413
+ error.email = 'Email is required';
414
+ } else if (!/^[^@]+@[^@]+$/.test(email)) {
415
+ error.email = 'Email is invalid';
416
+ }
417
+
418
+ if (typeof password !== 'string') {
419
+ error.password = 'Password is required';
420
+ }
421
+
422
+ if (error.email || error.password) {
423
+ return { error };
424
+ }
425
+
426
+ return {
427
+ value: { email, password },
428
+ };
429
+ },
430
+ });
431
+ ```
432
+
433
+ ---
434
+
435
+ ### validateConstraint
436
+
437
+ This enable Constraint Validation with ability to enable custom constraint using data-attribute and customizing error messages. By default, the error message would be the attribute that triggered the error (e.g. `required` / `type` / 'minLength' etc).
438
+
439
+ ```tsx
440
+ import { useForm, validateConstraint } from '@conform-to/react';
441
+ import { Form } from 'react-router-dom';
442
+
443
+ export default function SignupForm() {
444
+ const [form, { email, password, confirmPassword }] = useForm({
445
+ onValidate(context) {
446
+ // This enables validating each field based on the validity state and custom cosntraint if defined
447
+ return validateConstraint(
448
+ ...context,
449
+ constraint: {
450
+ // Define custom constraint
451
+ match(value, { formData, attributeValue }) {
452
+ // Check if the value of the field match the value of another field
453
+ return value === formData.get(attributeValue);
454
+ },
455
+ });
456
+ }
457
+ });
458
+
459
+ return (
460
+ <Form method="post" {...form.props}>
461
+ <div>
462
+ <label>Email</label>
463
+ <input
464
+ name="email"
465
+ type="email"
466
+ required
467
+ pattern="[^@]+@[^@]+\\.[^@]+"
468
+ />
469
+ {email.error === 'required' ? (
470
+ <div>Email is required</div>
471
+ ) : email.error === 'type' ? (
472
+ <div>Email is invalid</div>
473
+ ) : null}
474
+ </div>
475
+ <div>
476
+ <label>Password</label>
477
+ <input
478
+ name="password"
479
+ type="password"
480
+ required
481
+ />
482
+ {password.error === 'required' ? (
483
+ <div>Password is required</div>
484
+ ) : null}
485
+ </div>
486
+ <div>
487
+ <label>Confirm Password</label>
488
+ <input
489
+ name="confirmPassword"
490
+ type="password"
491
+ required
492
+ data-constraint-match="password"
493
+ />
494
+ {confirmPassword.error === 'required' ? (
495
+ <div>Confirm Password is required</div>
496
+ ) : confirmPassword.error === 'match' ? (
497
+ <div>Password does not match</div>
498
+ ) : null}
499
+ </div>
500
+ <button>Signup</button>
501
+ </Form>
502
+ );
503
+ }
504
+ ```
505
+
506
+ ---
507
+
395
508
  ### list
396
509
 
397
- It provides serveral helpers to configure a command button for [modifying a list](/docs/commands.md#modifying-a-list).
510
+ It provides serveral helpers to configure an intent button for [modifying a list](/docs/commands.md#modifying-a-list).
398
511
 
399
512
  ```tsx
400
513
  import { list } from '@conform-to/react';
@@ -427,7 +540,7 @@ function Example() {
427
540
 
428
541
  ### validate
429
542
 
430
- It returns the properties required to configure a command button for [validation](/docs/commands.md#validation).
543
+ It returns the properties required to configure an intent button for [validation](/docs/commands.md#validation).
431
544
 
432
545
  ```tsx
433
546
  import { validate } from '@conform-to/react';
@@ -463,7 +576,7 @@ import DragAndDrop from 'awesome-dnd-example';
463
576
 
464
577
  export default function Todos() {
465
578
  const [form, { tasks }] = useForm();
466
- const taskList = useFieldList(form.ref, tasks.config);
579
+ const taskList = useFieldList(form.ref, tasks);
467
580
 
468
581
  const handleDrop = (from, to) =>
469
582
  requestIntent(form.ref.current, list.reorder({ from, to }));
@@ -473,7 +586,7 @@ export default function Todos() {
473
586
  <DragAndDrop onDrop={handleDrop}>
474
587
  {taskList.map((task, index) => (
475
588
  <div key={task.key}>
476
- <input {...conform.input(task.config)} />
589
+ <input {...conform.input(task)} />
477
590
  </div>
478
591
  ))}
479
592
  </DragAndDrop>
@@ -485,81 +598,22 @@ export default function Todos() {
485
598
 
486
599
  ---
487
600
 
488
- ### getFormElements
601
+ ### isFieldElement
489
602
 
490
- It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field manually.
603
+ This is an utility for checking if the provided element is a form element (_input_ / _select_ / _textarea_ or _button_) which also works as a type guard.
491
604
 
492
605
  ```tsx
493
- import { useForm, parse, getFormElements } from '@conform-to/react';
494
-
495
- export default function LoginForm() {
496
- const [form] = useForm({
497
- onValidate({ form, formData }) {
498
- const submission = parse(formData);
499
-
500
- for (const element of getFormElements(form)) {
501
- switch (element.name) {
502
- case 'email': {
503
- if (element.validity.valueMissing) {
504
- submission.error.push([element.name, 'Email is required']);
505
- } else if (element.validity.typeMismatch) {
506
- submission.error.push([element.name, 'Email is invalid']);
507
- }
508
- break;
509
- }
510
- case 'password': {
511
- if (element.validity.valueMissing) {
512
- submission.error.push([element.name, 'Password is required']);
513
- }
514
- break;
515
- }
606
+ function Example() {
607
+ return (
608
+ <form
609
+ onFocus={(event) => {
610
+ if (isFieldElement(event.target)) {
611
+ // event.target is now considered one of the form elements type
516
612
  }
517
- }
518
-
519
- return submission;
520
- },
521
-
522
- // ....
523
- });
524
-
525
- // ...
613
+ }}
614
+ >
615
+ {/* ... */}
616
+ </form>
617
+ );
526
618
  }
527
619
  ```
528
-
529
- ---
530
-
531
- ### parse
532
-
533
- It parses the formData based on the [naming convention](/docs/submission).
534
-
535
- ```tsx
536
- import { parse } from '@conform-to/react';
537
-
538
- const formData = new FormData();
539
- const submission = parse(formData);
540
-
541
- console.log(submission);
542
- ```
543
-
544
- ---
545
-
546
- ### shouldValidate
547
-
548
- This helper checks if the scope of validation includes a specific field by checking the submission:
549
-
550
- ```tsx
551
- import { shouldValidate } from '@conform-to/react';
552
-
553
- /**
554
- * The submission intent give us hint on what should be valdiated.
555
- * If the intent is 'validate/:field', only the field with name matching must be validated.
556
- * If the intent is undefined, everything should be validated (Default submission)
557
- */
558
- const intent = 'validate/email';
559
-
560
- // This will log 'true'
561
- console.log(shouldValidate(intent, 'email'));
562
-
563
- // This will log 'false'
564
- console.log(shouldValidate(intent, 'password'));
565
- ```
package/helpers.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type FieldConfig, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom';
1
+ import { type FieldConfig, type Primitive, VALIDATION_UNDEFINED, VALIDATION_SKIPPED, INTENT } from '@conform-to/dom';
2
2
  import type { CSSProperties, HTMLInputTypeAttribute } from 'react';
3
- interface FieldProps {
3
+ interface FormControlProps {
4
4
  id?: string;
5
5
  name: string;
6
6
  form?: string;
@@ -8,11 +8,11 @@ interface FieldProps {
8
8
  autoFocus?: boolean;
9
9
  tabIndex?: number;
10
10
  style?: CSSProperties;
11
- 'aria-invalid': boolean;
12
11
  'aria-describedby'?: string;
12
+ 'aria-invalid'?: boolean;
13
13
  'aria-hidden'?: boolean;
14
14
  }
15
- interface InputProps<Schema> extends FieldProps {
15
+ interface InputProps<Schema> extends FormControlProps {
16
16
  type?: HTMLInputTypeAttribute;
17
17
  minLength?: number;
18
18
  maxLength?: number;
@@ -25,33 +25,30 @@ interface InputProps<Schema> extends FieldProps {
25
25
  defaultChecked?: boolean;
26
26
  defaultValue?: string;
27
27
  }
28
- interface SelectProps extends FieldProps {
28
+ interface SelectProps extends FormControlProps {
29
29
  defaultValue?: string | number | readonly string[] | undefined;
30
30
  multiple?: boolean;
31
31
  }
32
- interface TextareaProps extends FieldProps {
32
+ interface TextareaProps extends FormControlProps {
33
33
  minLength?: number;
34
34
  maxLength?: number;
35
35
  defaultValue?: string;
36
36
  }
37
- type InputOptions = {
38
- type: 'checkbox' | 'radio';
37
+ type BaseOptions = {
38
+ description?: boolean;
39
39
  hidden?: boolean;
40
+ };
41
+ type InputOptions = BaseOptions & ({
42
+ type: 'checkbox' | 'radio';
40
43
  value?: string;
41
44
  } | {
42
45
  type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden'>;
43
- hidden?: boolean;
44
46
  value?: never;
45
- };
46
- export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: {
47
+ });
48
+ export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: InputOptions & {
47
49
  type: 'file';
48
50
  }): InputProps<Schema>;
49
- export declare function input<Schema extends any>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>;
50
- export declare function select<Schema>(config: FieldConfig<Schema>, options?: {
51
- hidden?: boolean;
52
- }): SelectProps;
53
- export declare function textarea<Schema>(config: FieldConfig<Schema>, options?: {
54
- hidden?: boolean;
55
- }): TextareaProps;
56
- export declare const intent = "__intent__";
57
- export { VALIDATION_UNDEFINED, VALIDATION_SKIPPED };
51
+ export declare function input<Schema extends Primitive>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>;
52
+ export declare function select(config: FieldConfig<Primitive | Primitive[]>, options?: BaseOptions): SelectProps;
53
+ export declare function textarea(config: FieldConfig<Primitive>, options?: BaseOptions): TextareaProps;
54
+ export { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED };