@conform-to/react 0.5.0 → 0.6.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
@@ -9,8 +9,11 @@
9
9
  - [useForm](#useform)
10
10
  - [useFieldset](#usefieldset)
11
11
  - [useFieldList](#usefieldlist)
12
- - [useControlledInput](#usecontrolledinput)
12
+ - [useInputEvent](#useinputevent)
13
13
  - [conform](#conform)
14
+ - [list](#list)
15
+ - [validate](#validate)
16
+ - [requestIntent](#requestintent)
14
17
  - [getFormElements](#getformelements)
15
18
  - [hasError](#haserror)
16
19
  - [parse](#parse)
@@ -39,14 +42,6 @@ function LoginForm() {
39
42
  */
40
43
  id: undefined,
41
44
 
42
- /**
43
- * Validation mode.
44
- * Support "client-only" or "server-validation".
45
- *
46
- * Default to `client-only`.
47
- */
48
- mode: 'client-only',
49
-
50
45
  /**
51
46
  * Define when the error should be reported initially.
52
47
  * Support "onSubmit", "onChange", "onBlur".
@@ -278,34 +273,40 @@ function Example() {
278
273
 
279
274
  ---
280
275
 
281
- ### useControlledInput
276
+ ### useInputEvent
282
277
 
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.
278
+ It returns a ref object and a set of helpers that dispatch corresponding dom event.
284
279
 
285
280
  ```tsx
286
- import { useForm, useControlledInput } from '@conform-to/react';
281
+ import { useForm, useInputEvent } from '@conform-to/react';
287
282
  import { Select, MenuItem } from '@mui/material';
288
- import { useRef } from 'react';
283
+ import { useState, useRef } from 'react';
289
284
 
290
285
  function MuiForm() {
291
286
  const [form, { category }] = useForm();
292
- const [inputProps, control] = useControlledInput(category.config);
287
+ const [value, setValue] = useState(category.config.defaultValue ?? '');
288
+ const [ref, control] = useInputEvent({
289
+ onReset: () => setValue(category.config.defaultValue ?? ''),
290
+ });
291
+ const inputRef = useRef<HTMLInputElement>(null);
293
292
 
294
293
  return (
295
294
  <form {...form.props}>
296
295
  {/* Render a shadow input somewhere */}
297
- <input {...inputProps} />
296
+ <input
297
+ ref={ref}
298
+ {...conform.input(category.config, { hidden: true })}
299
+ onChange={(e) => setValue(e.target.value)}
300
+ onFocus={() => inputRef.current?.focus()}
301
+ />
298
302
 
299
303
  {/* MUI Select is a controlled component */}
300
304
  <TextField
301
305
  label="Category"
302
- inputRef={control.ref}
303
- value={control.value}
304
- onChange={control.onChange}
305
- onBlur={control.onBlur}
306
- inputProps={{
307
- onInvalid: control.onInvalid,
308
- }}
306
+ inputRef={inputRef}
307
+ value={value}
308
+ onChange={control.change}
309
+ onBlur={control.blur}
309
310
  select
310
311
  >
311
312
  <MenuItem value="">Please select</MenuItem>
@@ -446,9 +447,9 @@ function Example() {
446
447
 
447
448
  ---
448
449
 
449
- ### requestCommand
450
+ ### requestIntent
450
451
 
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
+ It lets you [trigger an intent](/docs/commands.md#triggering-an-intent) without requiring users to click on a button. It supports both [list](#list) and [validate](#validate) intent.
452
453
 
453
454
  ```tsx
454
455
  import {
@@ -456,7 +457,7 @@ import {
456
457
  useFieldList,
457
458
  conform,
458
459
  list,
459
- requestCommand,
460
+ requestIntent,
460
461
  } from '@conform-to/react';
461
462
  import DragAndDrop from 'awesome-dnd-example';
462
463
 
@@ -465,7 +466,7 @@ export default function Todos() {
465
466
  const taskList = useFieldList(form.ref, tasks.config);
466
467
 
467
468
  const handleDrop = (from, to) =>
468
- requestCommand(form.ref.current, list.reorder({ from, to }));
469
+ requestIntent(form.ref.current, list.reorder({ from, to }));
469
470
 
470
471
  return (
471
472
  <form {...form.props}>
@@ -527,27 +528,6 @@ export default function LoginForm() {
527
528
 
528
529
  ---
529
530
 
530
- ### hasError
531
-
532
- This helper checks if there is any message defined in error array with the provided name.
533
-
534
- ```ts
535
- import { hasError } from '@conform-to/react';
536
-
537
- /**
538
- * Assume the error looks like this:
539
- */
540
- const error = [['email', 'Email is required']];
541
-
542
- // This will log `true`
543
- console.log(hasError(error, 'email'));
544
-
545
- // This will log `false`
546
- console.log(hasError(error, 'password'));
547
- ```
548
-
549
- ---
550
-
551
531
  ### parse
552
532
 
553
533
  It parses the formData based on the [naming convention](/docs/submission).
@@ -571,20 +551,15 @@ This helper checks if the scope of validation includes a specific field by check
571
551
  import { shouldValidate } from '@conform-to/react';
572
552
 
573
553
  /**
574
- * The submission type and intent give us hint on what should be valdiated.
575
- * If the type is 'validate', only the field with name matching the metadata must be validated.
576
- * If the type is 'submit', everything should be validated (Default submission)
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)
577
557
  */
578
- const submission = {
579
- context: 'validate',
580
- intent: 'email',
581
- value: {},
582
- error: [],
583
- };
558
+ const intent = 'validate/email';
584
559
 
585
560
  // This will log 'true'
586
- console.log(shouldValidate(submission, 'email'));
561
+ console.log(shouldValidate(intent, 'email'));
587
562
 
588
563
  // This will log 'false'
589
- console.log(shouldValidate(submission, 'password'));
564
+ console.log(shouldValidate(intent, 'password'));
590
565
  ```
@@ -24,6 +24,7 @@ function _objectSpread2(target) {
24
24
  return target;
25
25
  }
26
26
  function _defineProperty(obj, key, value) {
27
+ key = _toPropertyKey(key);
27
28
  if (key in obj) {
28
29
  Object.defineProperty(obj, key, {
29
30
  value: value,
@@ -36,6 +37,22 @@ function _defineProperty(obj, key, value) {
36
37
  }
37
38
  return obj;
38
39
  }
40
+ function _toPrimitive(input, hint) {
41
+ if (typeof input !== "object" || input === null) return input;
42
+ var prim = input[Symbol.toPrimitive];
43
+ if (prim !== undefined) {
44
+ var res = prim.call(input, hint || "default");
45
+ if (typeof res !== "object") return res;
46
+ throw new TypeError("@@toPrimitive must return a primitive value.");
47
+ }
48
+ return (hint === "string" ? String : Number)(input);
49
+ }
50
+ function _toPropertyKey(arg) {
51
+ var key = _toPrimitive(arg, "string");
52
+ return typeof key === "symbol" ? key : String(key);
53
+ }
39
54
 
40
55
  exports.defineProperty = _defineProperty;
41
56
  exports.objectSpread2 = _objectSpread2;
57
+ exports.toPrimitive = _toPrimitive;
58
+ exports.toPropertyKey = _toPropertyKey;
package/helpers.d.ts CHANGED
@@ -1,13 +1,16 @@
1
- import type { FieldConfig } from '@conform-to/dom';
2
- import type { HTMLInputTypeAttribute } from 'react';
1
+ import { type FieldConfig, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom';
2
+ import type { CSSProperties, HTMLInputTypeAttribute } from 'react';
3
3
  interface FieldProps {
4
4
  id?: string;
5
5
  name: string;
6
6
  form?: string;
7
7
  required?: boolean;
8
8
  autoFocus?: boolean;
9
+ tabIndex?: number;
10
+ style?: CSSProperties;
9
11
  'aria-invalid': boolean;
10
12
  'aria-describedby'?: string;
13
+ 'aria-hidden'?: boolean;
11
14
  }
12
15
  interface InputProps<Schema> extends FieldProps {
13
16
  type?: HTMLInputTypeAttribute;
@@ -31,20 +34,24 @@ interface TextareaProps extends FieldProps {
31
34
  maxLength?: number;
32
35
  defaultValue?: string;
33
36
  }
34
- declare type InputOptions = {
37
+ type InputOptions = {
35
38
  type: 'checkbox' | 'radio';
39
+ hidden?: boolean;
36
40
  value?: string;
37
41
  } | {
38
- type: 'file';
39
- value?: never;
40
- } | {
41
- type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden' | 'file'>;
42
+ type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden'>;
43
+ hidden?: boolean;
42
44
  value?: never;
43
45
  };
44
46
  export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: {
45
47
  type: 'file';
46
48
  }): InputProps<Schema>;
47
49
  export declare function input<Schema extends any>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>;
48
- export declare function select<Schema>(config: FieldConfig<Schema>): SelectProps;
49
- export declare function textarea<Schema>(config: FieldConfig<Schema>): TextareaProps;
50
- export {};
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 };
package/helpers.js CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var dom = require('@conform-to/dom');
6
+
7
+ /**
8
+ * Style to make the input element visually hidden
9
+ * Based on the `sr-only` class from tailwindcss
10
+ */
11
+ var hiddenStyle = {
12
+ position: 'absolute',
13
+ width: '1px',
14
+ height: '1px',
15
+ padding: 0,
16
+ margin: '-1px',
17
+ overflow: 'hidden',
18
+ clip: 'rect(0,0,0,0)',
19
+ whiteSpace: 'nowrap',
20
+ border: 0
21
+ };
5
22
  function input(config) {
6
23
  var _config$initialError;
7
24
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -21,7 +38,12 @@ function input(config) {
21
38
  'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length),
22
39
  'aria-describedby': config.errorId
23
40
  };
24
- if (config.initialError && config.initialError.length > 0) {
41
+ if (options !== null && options !== void 0 && options.hidden) {
42
+ attributes.style = hiddenStyle;
43
+ attributes.tabIndex = -1;
44
+ attributes['aria-hidden'] = true;
45
+ }
46
+ if (config.initialError && Object.entries(config.initialError).length > 0) {
25
47
  attributes.autoFocus = true;
26
48
  }
27
49
  if (options.type === 'checkbox' || options.type === 'radio') {
@@ -33,7 +55,7 @@ function input(config) {
33
55
  }
34
56
  return attributes;
35
57
  }
36
- function select(config) {
58
+ function select(config, options) {
37
59
  var _config$defaultValue, _config$initialError2;
38
60
  var attributes = {
39
61
  id: config.id,
@@ -45,12 +67,17 @@ function select(config) {
45
67
  'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length),
46
68
  'aria-describedby': config.errorId
47
69
  };
48
- if (config.initialError && config.initialError.length > 0) {
70
+ if (options !== null && options !== void 0 && options.hidden) {
71
+ attributes.style = hiddenStyle;
72
+ attributes.tabIndex = -1;
73
+ attributes['aria-hidden'] = true;
74
+ }
75
+ if (config.initialError && Object.entries(config.initialError).length > 0) {
49
76
  attributes.autoFocus = true;
50
77
  }
51
78
  return attributes;
52
79
  }
53
- function textarea(config) {
80
+ function textarea(config, options) {
54
81
  var _config$defaultValue2, _config$initialError3;
55
82
  var attributes = {
56
83
  id: config.id,
@@ -64,12 +91,27 @@ function textarea(config) {
64
91
  'aria-invalid': Boolean((_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : _config$initialError3.length),
65
92
  'aria-describedby': config.errorId
66
93
  };
67
- if (config.initialError && config.initialError.length > 0) {
94
+ if (options !== null && options !== void 0 && options.hidden) {
95
+ attributes.style = hiddenStyle;
96
+ attributes.tabIndex = -1;
97
+ attributes['aria-hidden'] = true;
98
+ }
99
+ if (config.initialError && Object.entries(config.initialError).length > 0) {
68
100
  attributes.autoFocus = true;
69
101
  }
70
102
  return attributes;
71
103
  }
104
+ var intent = '__intent__';
72
105
 
106
+ Object.defineProperty(exports, 'VALIDATION_SKIPPED', {
107
+ enumerable: true,
108
+ get: function () { return dom.VALIDATION_SKIPPED; }
109
+ });
110
+ Object.defineProperty(exports, 'VALIDATION_UNDEFINED', {
111
+ enumerable: true,
112
+ get: function () { return dom.VALIDATION_UNDEFINED; }
113
+ });
73
114
  exports.input = input;
115
+ exports.intent = intent;
74
116
  exports.select = select;
75
117
  exports.textarea = textarea;
package/hooks.d.ts CHANGED
@@ -1,15 +1,11 @@
1
- import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type Primitive, type Submission } from '@conform-to/dom';
2
- import { type InputHTMLAttributes, type FormEvent, type RefObject } from 'react';
3
- export interface FormConfig<Schema extends Record<string, any>> {
1
+ import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type FormMethod, type FormEncType, type Submission } from '@conform-to/dom';
2
+ import { type FormEvent, type RefObject } from 'react';
3
+ export interface FormConfig<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission> {
4
4
  /**
5
5
  * If the form id is provided, Id for label,
6
6
  * input and error elements will be derived.
7
7
  */
8
8
  id?: string;
9
- /**
10
- * Validation mode. Default to `client-only`.
11
- */
12
- mode?: 'client-only' | 'server-validation';
13
9
  /**
14
10
  * Define when the error should be reported initially.
15
11
  * Support "onSubmit", "onChange", "onBlur".
@@ -24,7 +20,7 @@ export interface FormConfig<Schema extends Record<string, any>> {
24
20
  /**
25
21
  * An object describing the state from the last submission
26
22
  */
27
- state?: Submission<Schema>;
23
+ state?: Submission;
28
24
  /**
29
25
  * An object describing the constraint of each field
30
26
  */
@@ -47,14 +43,17 @@ export interface FormConfig<Schema extends Record<string, any>> {
47
43
  onValidate?: ({ form, formData, }: {
48
44
  form: HTMLFormElement;
49
45
  formData: FormData;
50
- }) => Submission<Schema>;
46
+ }) => ClientSubmission;
51
47
  /**
52
48
  * The submit event handler of the form. It will be called
53
49
  * only when the form is considered valid.
54
50
  */
55
51
  onSubmit?: (event: FormEvent<HTMLFormElement>, context: {
56
52
  formData: FormData;
57
- submission: Submission<Schema>;
53
+ submission: ClientSubmission;
54
+ action: string;
55
+ encType: FormEncType;
56
+ method: FormMethod;
58
57
  }) => void;
59
58
  }
60
59
  /**
@@ -79,18 +78,19 @@ interface Form<Schema extends Record<string, any>> {
79
78
  *
80
79
  * @see https://conform.guide/api/react#useform
81
80
  */
82
- export declare function useForm<Schema extends Record<string, any>>(config?: FormConfig<Schema>): [Form<Schema>, Fieldset<Schema>];
81
+ export declare function useForm<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission>(config?: FormConfig<Schema, ClientSubmission>): [Form<Schema>, Fieldset<Schema>];
83
82
  /**
84
83
  * All the information of the field, including state and config.
85
84
  */
86
- export declare type Field<Schema> = {
85
+ export type Field<Schema> = {
87
86
  config: FieldConfig<Schema>;
88
87
  error?: string;
88
+ errors?: string[];
89
89
  };
90
90
  /**
91
91
  * A set of field information.
92
92
  */
93
- export declare type Fieldset<Schema extends Record<string, any>> = {
93
+ export type Fieldset<Schema extends Record<string, any>> = {
94
94
  [Key in keyof Schema]-?: Field<Schema[Key]>;
95
95
  };
96
96
  export interface FieldsetConfig<Schema extends Record<string, any>> {
@@ -105,7 +105,7 @@ export interface FieldsetConfig<Schema extends Record<string, any>> {
105
105
  /**
106
106
  * An object describing the initial error of each field
107
107
  */
108
- initialError?: Array<[string, string]>;
108
+ initialError?: Record<string, string | string[]>;
109
109
  /**
110
110
  * An object describing the constraint of each field
111
111
  */
@@ -131,32 +131,30 @@ export declare function useFieldset<Schema extends Record<string, any>>(ref: Ref
131
131
  export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Array<Payload>>): Array<{
132
132
  key: string;
133
133
  error: string | undefined;
134
+ errors: string[] | undefined;
134
135
  config: FieldConfig<Payload>;
135
136
  }>;
136
- interface ShadowInputProps extends InputHTMLAttributes<HTMLInputElement> {
137
- ref: RefObject<HTMLInputElement>;
138
- }
139
- interface InputControl<Element extends {
140
- focus: () => void;
141
- }> {
142
- ref: RefObject<Element>;
143
- value: string;
144
- onChange: (eventOrValue: {
137
+ interface InputControl {
138
+ change: (eventOrValue: {
145
139
  target: {
146
140
  value: string;
147
141
  };
148
142
  } | string) => void;
149
- onBlur: () => void;
150
- onInvalid: (event: FormEvent<FieldElement>) => void;
143
+ focus: () => void;
144
+ blur: () => void;
151
145
  }
152
146
  /**
153
- * Returns the properties required to configure a shadow input for validation.
154
- * This is particular useful when integrating dropdown and datepicker whichs
155
- * introduces custom input mode.
147
+ * Returns a ref object and a set of helpers that dispatch corresponding dom event.
156
148
  *
157
- * @see https://conform.guide/api/react#usecontrolledinput
149
+ * @see https://conform.guide/api/react#useinputevent
158
150
  */
159
- export declare function useControlledInput<Element extends {
160
- focus: () => void;
161
- } = HTMLInputElement, Schema extends Primitive = Primitive>(config: FieldConfig<Schema>): [ShadowInputProps, InputControl<Element>];
151
+ export declare function useInputEvent<RefShape extends FieldElement = HTMLInputElement>(options?: {
152
+ onSubmit?: (event: SubmitEvent) => void;
153
+ onReset?: (event: Event) => void;
154
+ }): [RefObject<RefShape>, InputControl];
155
+ export declare function useInputEvent<RefShape extends Exclude<any, FieldElement>>(options: {
156
+ getElement: (ref: RefShape | null) => FieldElement | null | undefined;
157
+ onSubmit?: (event: SubmitEvent) => void;
158
+ onReset?: (event: Event) => void;
159
+ }): [RefObject<RefShape>, InputControl];
162
160
  export {};