@conform-to/react 1.0.0 → 1.0.2

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 CHANGED
@@ -8,25 +8,30 @@
8
8
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
9
9
 
10
10
 
11
- Version 1.0.0-rc.1 / License MIT / Copyright (c) 2024 Edmund Hung
11
+ Version 1.0.2 / License MIT / Copyright (c) 2024 Edmund Hung
12
12
 
13
13
  A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
14
14
 
15
- > Getting Started
15
+ # Getting Started
16
16
 
17
17
  Check out the overview and tutorial at our website https://conform.guide
18
18
 
19
- > Documentation
19
+ # Features
20
20
 
21
- The documentation is divided into several sections:
21
+ - Progressive enhancement first APIs
22
+ - Type-safe field inference
23
+ - Fine-grained subscription
24
+ - Built-in accessibility helpers
25
+ - Automatic type coercion with Zod
22
26
 
23
- * Overview: https://conform.guide/overview
24
- * Examples: https://conform.guide/examples
25
- * Complex structures: https://conform.guide/complex-structures
26
- * UI Integrations: https://conform.guide/integrations
27
- * Accessibility Guide: https://conform.guide/accessibility
28
- * API Reference: https://conform.guide/references
27
+ # Documentation
29
28
 
30
- > Support
29
+ - Validation: https://conform.guide/validation
30
+ - Nested object and Array: https://conform.guide/complex-structures
31
+ - UI Integrations: https://conform.guide/integration/ui-libraries
32
+ - Intent button: https://conform.guide/intent-button
33
+ - Accessibility Guide: https://conform.guide/accessibility
34
+
35
+ # Support
31
36
 
32
37
  To report a bug, please open an issue on the repository at https://github.com/edmundhung/conform. For feature requests and questions, you can post them in the Discussions section.
package/context.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { type Constraint, type FormId, type FieldName, type FormContext as BaseFormContext, type FormValue, type FormState, type Intent, type SubscriptionScope, type SubscriptionSubject, type UnionKeyof, type UnionKeyType, type FormOptions as BaseFormOptions } from '@conform-to/dom';
1
+ import { type Constraint, type Combine, type FormId, type FieldName, type FormContext as BaseFormContext, type FormValue, type FormState, type Intent, type SubscriptionScope, type SubscriptionSubject, type FormOptions as BaseFormOptions } from '@conform-to/dom';
2
2
  import { type FormEvent, type ReactElement, type ReactNode, type MutableRefObject } from 'react';
3
3
  export type Pretty<T> = {
4
4
  [K in keyof T]: T[K];
5
5
  } & {};
6
- export type Primitive = string | number | boolean | Date | File | null | undefined;
6
+ export type Primitive = string | number | bigint | boolean | Date | File | null | undefined;
7
7
  export type Metadata<Schema, FormSchema extends Record<string, unknown>, FormError = string[]> = {
8
8
  key: string | undefined;
9
9
  id: string;
@@ -21,19 +21,22 @@ export type FormMetadata<Schema extends Record<string, unknown> = Record<string,
21
21
  id: FormId<Schema, FormError>;
22
22
  context: Wrapped<FormContext<Schema, FormError>>;
23
23
  status?: 'success' | 'error';
24
- getFieldset: () => {
25
- [Key in UnionKeyof<Schema>]: FieldMetadata<UnionKeyType<Schema, Key>, Schema, FormError>;
26
- };
24
+ getFieldset: () => Required<{
25
+ [Key in keyof Combine<Schema>]: FieldMetadata<Combine<Schema>[Key], Schema, FormError>;
26
+ }>;
27
27
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
28
28
  noValidate: boolean;
29
29
  };
30
+ type SubfieldMetadata<Schema, FormSchema extends Record<string, any>, FormError, CombinedSchema = Combine<Schema>> = Exclude<Schema, undefined> extends Array<infer Item> ? {
31
+ getFieldList: () => Array<FieldMetadata<Item, FormSchema, FormError>>;
32
+ } : Exclude<Schema, undefined> extends Record<string, unknown> ? {
33
+ getFieldset: () => Required<{
34
+ [Key in keyof CombinedSchema]: FieldMetadata<CombinedSchema[Key], FormSchema, FormError>;
35
+ }>;
36
+ } : {};
30
37
  export type FieldMetadata<Schema = unknown, FormSchema extends Record<string, any> = Record<string, unknown>, FormError = string[]> = Metadata<Schema, FormSchema, FormError> & Constraint & {
31
38
  formId: FormId<FormSchema, FormError>;
32
- getFieldset: unknown extends Schema ? () => unknown : Schema extends Primitive | Array<any> ? never : () => {
33
- [Key in UnionKeyof<Schema>]: FieldMetadata<UnionKeyType<Schema, Key>, FormSchema, FormError>;
34
- };
35
- getFieldList: unknown extends Schema ? () => unknown : Schema extends Array<infer Item> ? () => Array<FieldMetadata<Item, FormSchema, FormError>> : never;
36
- };
39
+ } & SubfieldMetadata<Schema, FormSchema, FormError>;
37
40
  export declare const Form: import("react").Context<FormContext<any, string[], any>[]>;
38
41
  declare const wrappedSymbol: unique symbol;
39
42
  export type Wrapped<Type> = {
package/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import type { FormMetadata, FieldMetadata, Metadata, Pretty, Primitive } from './context';
2
+ import type { FormMetadata, FieldMetadata, Metadata, Pretty } from './context';
3
3
  type FormControlProps = {
4
4
  key: string | undefined;
5
5
  id: string;
@@ -138,7 +138,7 @@ export declare function getFormControlProps<Schema>(metadata: FieldMetadata<Sche
138
138
  * <input {...getInputProps(metadata, { type: 'radio', value: false })} />
139
139
  * ```
140
140
  */
141
- export declare function getInputProps<Schema extends Primitive | File[]>(metadata: FieldMetadata<Schema, any, any>, options: InputOptions): InputProps;
141
+ export declare function getInputProps<Schema>(metadata: FieldMetadata<Schema, any, any>, options: InputOptions): InputProps;
142
142
  /**
143
143
  * Derives the properties of a select element based on the field metadata,
144
144
  * including common form control attributes like `key`, `id`, `name`, `form`, `autoFocus`, `aria-invalid`, `aria-describedby`
@@ -154,7 +154,7 @@ export declare function getInputProps<Schema extends Primitive | File[]>(metadat
154
154
  * <select {...getSelectProps(metadata, { value: false })} />
155
155
  * ```
156
156
  */
157
- export declare function getSelectProps<Schema extends Exclude<Primitive, File> | Array<Exclude<Primitive, File>> | undefined>(metadata: FieldMetadata<Schema, any, any>, options?: SelectOptions): SelectProps;
157
+ export declare function getSelectProps<Schema>(metadata: FieldMetadata<Schema, any, any>, options?: SelectOptions): SelectProps;
158
158
  /**
159
159
  * Derives the properties of a textarea element based on the field metadata,
160
160
  * including common form control attributes like `key`, `id`, `name`, `form`, `autoFocus`, `aria-invalid`, `aria-describedby`
@@ -170,7 +170,7 @@ export declare function getSelectProps<Schema extends Exclude<Primitive, File> |
170
170
  * <textarea {...getTextareaProps(metadata, { value: false })} />
171
171
  * ```
172
172
  */
173
- export declare function getTextareaProps<Schema extends Exclude<Primitive, File> | undefined>(metadata: FieldMetadata<Schema, any, any>, options?: TextareaOptions): TextareaProps;
173
+ export declare function getTextareaProps<Schema>(metadata: FieldMetadata<Schema, any, any>, options?: TextareaOptions): TextareaProps;
174
174
  /**
175
175
  * Derives the properties of a collection of checkboxes or radio buttons based on the field metadata,
176
176
  * including common form control attributes like `key`, `id`, `name`, `form`, `autoFocus`, `aria-invalid`, `aria-describedby` and `required`.
package/helpers.js CHANGED
@@ -141,7 +141,8 @@ function getSelectProps(metadata) {
141
141
  multiple: metadata.multiple
142
142
  });
143
143
  if (typeof options.value === 'undefined' || options.value) {
144
- props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : metadata.initialValue;
144
+ var _metadata$initialValu;
145
+ props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : (_metadata$initialValu = metadata.initialValue) === null || _metadata$initialValu === void 0 ? void 0 : _metadata$initialValu.toString();
145
146
  }
146
147
  return simplify(props);
147
148
  }
@@ -168,7 +169,8 @@ function getTextareaProps(metadata) {
168
169
  maxLength: metadata.maxLength
169
170
  });
170
171
  if (typeof options.value === 'undefined' || options.value) {
171
- props.defaultValue = metadata.initialValue;
172
+ var _metadata$initialValu2;
173
+ props.defaultValue = (_metadata$initialValu2 = metadata.initialValue) === null || _metadata$initialValu2 === void 0 ? void 0 : _metadata$initialValu2.toString();
172
174
  }
173
175
  return simplify(props);
174
176
  }
package/helpers.mjs CHANGED
@@ -137,7 +137,8 @@ function getSelectProps(metadata) {
137
137
  multiple: metadata.multiple
138
138
  });
139
139
  if (typeof options.value === 'undefined' || options.value) {
140
- props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : metadata.initialValue;
140
+ var _metadata$initialValu;
141
+ props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : (_metadata$initialValu = metadata.initialValue) === null || _metadata$initialValu === void 0 ? void 0 : _metadata$initialValu.toString();
141
142
  }
142
143
  return simplify(props);
143
144
  }
@@ -164,7 +165,8 @@ function getTextareaProps(metadata) {
164
165
  maxLength: metadata.maxLength
165
166
  });
166
167
  if (typeof options.value === 'undefined' || options.value) {
167
- props.defaultValue = metadata.initialValue;
168
+ var _metadata$initialValu2;
169
+ props.defaultValue = (_metadata$initialValu2 = metadata.initialValue) === null || _metadata$initialValu2 === void 0 ? void 0 : _metadata$initialValu2.toString();
168
170
  }
169
171
  return simplify(props);
170
172
  }
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { type Submission, type SubmissionResult, type DefaultValue, type Intent, type FormId, type FieldName, parse, } from '@conform-to/dom';
2
2
  export { type FieldMetadata, type FormMetadata, FormProvider, FormStateInput, } from './context';
3
3
  export { useForm, useFormMetadata, useField } from './hooks';
4
- export { useInputControl } from './integrations';
4
+ export { Control as unstable_Control, useControl as unstable_useControl, useInputControl, } from './integrations';
5
5
  export { getFormProps, getFieldsetProps, getInputProps, getSelectProps, getTextareaProps, getCollectionProps, } from './helpers';
package/index.js CHANGED
@@ -19,6 +19,8 @@ exports.FormStateInput = context.FormStateInput;
19
19
  exports.useField = hooks.useField;
20
20
  exports.useForm = hooks.useForm;
21
21
  exports.useFormMetadata = hooks.useFormMetadata;
22
+ exports.unstable_Control = integrations.Control;
23
+ exports.unstable_useControl = integrations.useControl;
22
24
  exports.useInputControl = integrations.useInputControl;
23
25
  exports.getCollectionProps = helpers.getCollectionProps;
24
26
  exports.getFieldsetProps = helpers.getFieldsetProps;
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { parse } from '@conform-to/dom';
2
2
  export { FormProvider, FormStateInput } from './context.mjs';
3
3
  export { useField, useForm, useFormMetadata } from './hooks.mjs';
4
- export { useInputControl } from './integrations.mjs';
4
+ export { Control as unstable_Control, useControl as unstable_useControl, useInputControl } from './integrations.mjs';
5
5
  export { getCollectionProps, getFieldsetProps, getFormProps, getInputProps, getSelectProps, getTextareaProps } from './helpers.mjs';
package/integrations.d.ts CHANGED
@@ -1,17 +1,45 @@
1
- import { type FieldElement } from '@conform-to/dom';
2
- import { type Key } from 'react';
3
- export type InputControl = {
4
- value: string | undefined;
5
- change: (value: string) => void;
1
+ import { type Key, type RefCallback } from 'react';
2
+ export declare function getFormElement(formId: string): HTMLFormElement | null;
3
+ export declare function getFieldElements(form: HTMLFormElement | null, name: string): Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
4
+ export declare function getEventTarget(form: HTMLFormElement | null, name: string, value?: string | string[]): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
5
+ export declare function createDummySelect(form: HTMLFormElement, name: string, value?: string | string[] | undefined): HTMLSelectElement;
6
+ export declare function isDummySelect(element: HTMLElement): element is HTMLSelectElement;
7
+ export declare function updateFieldValue(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, value: string | string[]): void;
8
+ export declare function useInputEvent(): {
9
+ change(value: string | string[]): void;
10
+ focus(): void;
11
+ blur(): void;
12
+ register: RefCallback<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | undefined>;
13
+ };
14
+ export declare function useInputValue<Value extends string | string[] | Array<string | undefined>>(options: {
15
+ key?: Key | null | undefined;
16
+ initialValue?: Value | undefined;
17
+ }): readonly [(Value extends string ? Value : string | string[]) | undefined, import("react").Dispatch<import("react").SetStateAction<(Value extends string ? Value : string | string[]) | undefined>>];
18
+ export declare function useControl<Value extends string | string[] | Array<string | undefined>>(meta: {
19
+ key?: Key | null | undefined;
20
+ initialValue?: Value | undefined;
21
+ }): {
22
+ register: RefCallback<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | undefined>;
23
+ value: (Value extends string ? Value : string | string[]) | undefined;
24
+ change: (value: Value extends string ? Value : string | string[]) => void;
6
25
  focus: () => void;
7
26
  blur: () => void;
8
27
  };
9
- export declare function getFieldElement(formId: string, name: string, match?: (element: FieldElement) => boolean): FieldElement | null;
10
- export declare function getEventTarget(formId: string, name: string): FieldElement;
11
- export type InputControlOptions = {
28
+ export declare function useInputControl<Value extends string | string[] | Array<string | undefined>>(meta: {
12
29
  key?: Key | null | undefined;
13
30
  name: string;
14
31
  formId: string;
15
- initialValue?: string | undefined;
32
+ initialValue?: Value | undefined;
33
+ }): {
34
+ value: (Value extends string ? Value : string | string[]) | undefined;
35
+ change: import("react").Dispatch<import("react").SetStateAction<(Value extends string ? Value : string | string[]) | undefined>>;
36
+ focus: () => void;
37
+ blur: () => void;
16
38
  };
17
- export declare function useInputControl(metaOrOptions: InputControlOptions): InputControl;
39
+ export declare function Control<Value extends string | string[] | Array<string | undefined>>(props: {
40
+ meta: {
41
+ key?: Key | null | undefined;
42
+ initialValue?: Value | undefined;
43
+ };
44
+ render: (control: ReturnType<typeof useControl<Value>>) => React.ReactNode;
45
+ }): import("react").ReactNode;
package/integrations.js CHANGED
@@ -2,53 +2,120 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
- var dom = require('@conform-to/dom');
7
5
  var react = require('react');
8
6
 
9
- function getFieldElement(formId, name) {
10
- var _document$forms$named;
11
- var match = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : () => true;
12
- var element = (_document$forms$named = document.forms.namedItem(formId)) === null || _document$forms$named === void 0 ? void 0 : _document$forms$named.elements.namedItem(name);
13
- if (element) {
14
- var items = element instanceof Element ? [element] : Array.from(element.values());
15
- for (var item of items) {
16
- if (dom.isFieldElement(item) && match(item)) {
17
- return item;
7
+ function getFormElement(formId) {
8
+ return document.forms.namedItem(formId);
9
+ }
10
+ function getFieldElements(form, name) {
11
+ var field = form === null || form === void 0 ? void 0 : form.elements.namedItem(name);
12
+ var elements = !field ? [] : field instanceof Element ? [field] : Array.from(field.values());
13
+ return elements.filter(element => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement);
14
+ }
15
+ function getEventTarget(form, name, value) {
16
+ var _elements$;
17
+ var elements = getFieldElements(form, name);
18
+ if (elements.length > 1) {
19
+ var options = typeof value === 'string' ? [value] : value;
20
+ for (var element of elements) {
21
+ if (typeof options !== 'undefined' && element instanceof HTMLInputElement && element.type === 'checkbox' && (element.checked ? options.includes(element.value) : !options.includes(element.value))) {
22
+ continue;
18
23
  }
24
+ return element;
19
25
  }
20
26
  }
21
- return null;
27
+ return (_elements$ = elements[0]) !== null && _elements$ !== void 0 ? _elements$ : null;
28
+ }
29
+ function createDummySelect(form, name, value) {
30
+ var select = document.createElement('select');
31
+ var options = typeof value === 'string' ? [value] : value !== null && value !== void 0 ? value : [];
32
+ select.name = name;
33
+ select.multiple = true;
34
+ select.dataset.conform = 'true';
35
+
36
+ // To make sure the input is hidden but still focusable
37
+ select.setAttribute('aria-hidden', 'true');
38
+ select.tabIndex = -1;
39
+ select.style.position = 'absolute';
40
+ select.style.width = '1px';
41
+ select.style.height = '1px';
42
+ select.style.padding = '0';
43
+ select.style.margin = '-1px';
44
+ select.style.overflow = 'hidden';
45
+ select.style.clip = 'rect(0,0,0,0)';
46
+ select.style.whiteSpace = 'nowrap';
47
+ select.style.border = '0';
48
+ for (var option of options) {
49
+ select.options.add(new Option(option, option, true, true));
50
+ }
51
+ form.appendChild(select);
52
+ return select;
22
53
  }
23
- function getEventTarget(formId, name) {
24
- var element = getFieldElement(formId, name);
25
- if (element) {
26
- return element;
54
+ function isDummySelect(element) {
55
+ return element.dataset.conform === 'true';
56
+ }
57
+ function updateFieldValue(element, value) {
58
+ if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
59
+ element.checked = Array.isArray(value) ? value.includes(element.value) : element.value === value;
60
+ } else if (element instanceof HTMLSelectElement && element.multiple) {
61
+ var selectedValue = Array.isArray(value) ? [...value] : [value];
62
+ for (var option of element.options) {
63
+ var index = selectedValue.indexOf(option.value);
64
+ var selected = index > -1;
65
+
66
+ // Update the selected state of the option
67
+ option.selected = selected;
68
+ // Remove the option from the selected array
69
+ if (selected) {
70
+ selectedValue.splice(index, 1);
71
+ }
72
+ }
73
+
74
+ // Add the remaining options to the select element only if it's a dummy element managed by conform
75
+ if (isDummySelect(element)) {
76
+ for (var _option of selectedValue) {
77
+ element.options.add(new Option(_option, _option, false, true));
78
+ }
79
+ }
80
+ } else if (element.value !== value) {
81
+ // No `change` event will be triggered on React if `element.value` is already updated
82
+
83
+ /**
84
+ * Triggering react custom change event
85
+ * Solution based on dom-testing-library
86
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
87
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
88
+ */
89
+ var {
90
+ set: valueSetter
91
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
92
+ var prototype = Object.getPrototypeOf(element);
93
+ var {
94
+ set: prototypeValueSetter
95
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
96
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
97
+ prototypeValueSetter.call(element, value);
98
+ } else {
99
+ if (valueSetter) {
100
+ valueSetter.call(element, value);
101
+ } else {
102
+ throw new Error('The given element does not have a value setter');
103
+ }
104
+ }
27
105
  }
28
- var form = document.forms.namedItem(formId);
29
- var input = document.createElement('input');
30
- input.type = 'hidden';
31
- input.name = name;
32
- form === null || form === void 0 || form.appendChild(input);
33
- return input;
34
106
  }
35
- function useInputControl(metaOrOptions) {
107
+ function useInputEvent() {
108
+ var ref = react.useRef(null);
36
109
  var eventDispatched = react.useRef({
37
110
  change: false,
38
111
  focus: false,
39
112
  blur: false
40
113
  });
41
- var [key, setKey] = react.useState(metaOrOptions.key);
42
- var [value, setValue] = react.useState(() => metaOrOptions.initialValue);
43
- if (key !== metaOrOptions.key) {
44
- setValue(metaOrOptions.initialValue);
45
- setKey(metaOrOptions.key);
46
- }
47
114
  react.useEffect(() => {
48
115
  var createEventListener = listener => {
49
116
  return event => {
50
- var element = getFieldElement(metaOrOptions.formId, metaOrOptions.name, element => element === event.target);
51
- if (element) {
117
+ var element = ref.current;
118
+ if (element && event.target === element) {
52
119
  eventDispatched.current[listener] = true;
53
120
  }
54
121
  };
@@ -64,83 +131,155 @@ function useInputControl(metaOrOptions) {
64
131
  document.removeEventListener('focusin', focusHandler, true);
65
132
  document.removeEventListener('focusout', blurHandler, true);
66
133
  };
67
- }, [metaOrOptions.formId, metaOrOptions.name]);
68
- var handlers = react.useMemo(() => {
134
+ }, [ref]);
135
+ return react.useMemo(() => {
69
136
  return {
70
137
  change(value) {
71
138
  if (!eventDispatched.current.change) {
72
- var _element = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
73
139
  eventDispatched.current.change = true;
74
- if (_element instanceof HTMLInputElement && (_element.type === 'checkbox' || _element.type === 'radio')) {
75
- _element.checked = _element.value === value;
76
- } else if (_element.value !== value) {
77
- // No change event will be triggered on React if `element.value` is already updated
140
+ var element = ref.current;
141
+ if (element) {
142
+ updateFieldValue(element, value);
78
143
 
79
- /**
80
- * Triggering react custom change event
81
- * Solution based on dom-testing-library
82
- * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
83
- * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
84
- */
85
- var {
86
- set: valueSetter
87
- } = Object.getOwnPropertyDescriptor(_element, 'value') || {};
88
- var prototype = Object.getPrototypeOf(_element);
89
- var {
90
- set: prototypeValueSetter
91
- } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
92
- if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
93
- prototypeValueSetter.call(_element, value);
94
- } else {
95
- if (valueSetter) {
96
- valueSetter.call(_element, value);
97
- } else {
98
- throw new Error('The given element does not have a value setter');
99
- }
100
- }
144
+ // Dispatch input event with the updated input value
145
+ element.dispatchEvent(new InputEvent('input', {
146
+ bubbles: true
147
+ }));
148
+ // Dispatch change event (necessary for select to update the selected option)
149
+ element.dispatchEvent(new Event('change', {
150
+ bubbles: true
151
+ }));
101
152
  }
102
-
103
- // Dispatch input event with the updated input value
104
- _element.dispatchEvent(new InputEvent('input', {
105
- bubbles: true
106
- }));
107
- // Dispatch change event (necessary for select to update the selected option)
108
- _element.dispatchEvent(new Event('change', {
109
- bubbles: true
110
- }));
111
153
  }
112
- setValue(value);
113
154
  eventDispatched.current.change = false;
114
155
  },
115
156
  focus() {
116
157
  if (!eventDispatched.current.focus) {
117
- var _element2 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
118
158
  eventDispatched.current.focus = true;
119
- _element2.dispatchEvent(new FocusEvent('focusin', {
120
- bubbles: true
121
- }));
122
- _element2.dispatchEvent(new FocusEvent('focus'));
159
+ var element = ref.current;
160
+ if (element) {
161
+ element.dispatchEvent(new FocusEvent('focusin', {
162
+ bubbles: true
163
+ }));
164
+ element.dispatchEvent(new FocusEvent('focus'));
165
+ }
123
166
  }
124
167
  eventDispatched.current.focus = false;
125
168
  },
126
169
  blur() {
127
170
  if (!eventDispatched.current.blur) {
128
- var _element3 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
129
171
  eventDispatched.current.blur = true;
130
- _element3.dispatchEvent(new FocusEvent('focusout', {
131
- bubbles: true
132
- }));
133
- _element3.dispatchEvent(new FocusEvent('blur'));
172
+ var element = ref.current;
173
+ if (element) {
174
+ element.dispatchEvent(new FocusEvent('focusout', {
175
+ bubbles: true
176
+ }));
177
+ element.dispatchEvent(new FocusEvent('blur'));
178
+ }
134
179
  }
135
180
  eventDispatched.current.blur = false;
181
+ },
182
+ register(element) {
183
+ ref.current = element;
136
184
  }
137
185
  };
138
- }, [metaOrOptions.formId, metaOrOptions.name]);
139
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, handlers), {}, {
140
- value
141
- });
186
+ }, []);
187
+ }
188
+ function useInputValue(options) {
189
+ var initializeValue = () => {
190
+ var _options$initialValue;
191
+ if (typeof options.initialValue === 'string') {
192
+ // @ts-expect-error FIXME: To ensure that the type of value is also `string | undefined` if initialValue is not an array
193
+ return options.initialValue;
194
+ }
195
+
196
+ // @ts-expect-error Same as above
197
+ return (_options$initialValue = options.initialValue) === null || _options$initialValue === void 0 ? void 0 : _options$initialValue.map(value => value !== null && value !== void 0 ? value : '');
198
+ };
199
+ var [key, setKey] = react.useState(options.key);
200
+ var [value, setValue] = react.useState(initializeValue);
201
+ if (key !== options.key) {
202
+ setValue(initializeValue);
203
+ setKey(options.key);
204
+ }
205
+ return [value, setValue];
206
+ }
207
+ function useControl(meta) {
208
+ var [value, setValue] = useInputValue(meta);
209
+ var {
210
+ register,
211
+ change,
212
+ focus,
213
+ blur
214
+ } = useInputEvent();
215
+ var handleChange = value => {
216
+ setValue(value);
217
+ change(value);
218
+ };
219
+ return {
220
+ register,
221
+ value,
222
+ change: handleChange,
223
+ focus,
224
+ blur
225
+ };
226
+ }
227
+ function useInputControl(meta) {
228
+ var [value, setValue] = useInputValue(meta);
229
+ var initializedRef = react.useRef(false);
230
+ var {
231
+ register,
232
+ change,
233
+ focus,
234
+ blur
235
+ } = useInputEvent();
236
+ react.useEffect(() => {
237
+ var form = getFormElement(meta.formId);
238
+ if (!form) {
239
+ // eslint-disable-next-line no-console
240
+ console.warn("useInputControl is unable to find form#".concat(meta.formId, " and identify if a dummy input is required"));
241
+ return;
242
+ }
243
+ var element = getEventTarget(form, meta.name);
244
+ if (!element && typeof value !== 'undefined' && (!Array.isArray(value) || value.length > 0)) {
245
+ element = createDummySelect(form, meta.name, value);
246
+ }
247
+ register(element);
248
+ if (!initializedRef.current) {
249
+ initializedRef.current = true;
250
+ } else {
251
+ change(value !== null && value !== void 0 ? value : '');
252
+ }
253
+ return () => {
254
+ register(null);
255
+ var elements = getFieldElements(form, meta.name);
256
+ for (var _element of elements) {
257
+ if (isDummySelect(_element)) {
258
+ _element.remove();
259
+ }
260
+ }
261
+ };
262
+ }, [meta.formId, meta.name, value, change, register]);
263
+ return {
264
+ value,
265
+ change: setValue,
266
+ focus,
267
+ blur
268
+ };
269
+ }
270
+ function Control(props) {
271
+ var control = useControl(props.meta);
272
+ return props.render(control);
142
273
  }
143
274
 
275
+ exports.Control = Control;
276
+ exports.createDummySelect = createDummySelect;
144
277
  exports.getEventTarget = getEventTarget;
145
- exports.getFieldElement = getFieldElement;
278
+ exports.getFieldElements = getFieldElements;
279
+ exports.getFormElement = getFormElement;
280
+ exports.isDummySelect = isDummySelect;
281
+ exports.updateFieldValue = updateFieldValue;
282
+ exports.useControl = useControl;
146
283
  exports.useInputControl = useInputControl;
284
+ exports.useInputEvent = useInputEvent;
285
+ exports.useInputValue = useInputValue;
package/integrations.mjs CHANGED
@@ -1,50 +1,117 @@
1
- import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
2
- import { isFieldElement } from '@conform-to/dom';
3
- import { useRef, useState, useEffect, useMemo } from 'react';
1
+ import { useRef, useEffect, useMemo, useState } from 'react';
4
2
 
5
- function getFieldElement(formId, name) {
6
- var _document$forms$named;
7
- var match = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : () => true;
8
- var element = (_document$forms$named = document.forms.namedItem(formId)) === null || _document$forms$named === void 0 ? void 0 : _document$forms$named.elements.namedItem(name);
9
- if (element) {
10
- var items = element instanceof Element ? [element] : Array.from(element.values());
11
- for (var item of items) {
12
- if (isFieldElement(item) && match(item)) {
13
- return item;
3
+ function getFormElement(formId) {
4
+ return document.forms.namedItem(formId);
5
+ }
6
+ function getFieldElements(form, name) {
7
+ var field = form === null || form === void 0 ? void 0 : form.elements.namedItem(name);
8
+ var elements = !field ? [] : field instanceof Element ? [field] : Array.from(field.values());
9
+ return elements.filter(element => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement);
10
+ }
11
+ function getEventTarget(form, name, value) {
12
+ var _elements$;
13
+ var elements = getFieldElements(form, name);
14
+ if (elements.length > 1) {
15
+ var options = typeof value === 'string' ? [value] : value;
16
+ for (var element of elements) {
17
+ if (typeof options !== 'undefined' && element instanceof HTMLInputElement && element.type === 'checkbox' && (element.checked ? options.includes(element.value) : !options.includes(element.value))) {
18
+ continue;
14
19
  }
20
+ return element;
15
21
  }
16
22
  }
17
- return null;
23
+ return (_elements$ = elements[0]) !== null && _elements$ !== void 0 ? _elements$ : null;
24
+ }
25
+ function createDummySelect(form, name, value) {
26
+ var select = document.createElement('select');
27
+ var options = typeof value === 'string' ? [value] : value !== null && value !== void 0 ? value : [];
28
+ select.name = name;
29
+ select.multiple = true;
30
+ select.dataset.conform = 'true';
31
+
32
+ // To make sure the input is hidden but still focusable
33
+ select.setAttribute('aria-hidden', 'true');
34
+ select.tabIndex = -1;
35
+ select.style.position = 'absolute';
36
+ select.style.width = '1px';
37
+ select.style.height = '1px';
38
+ select.style.padding = '0';
39
+ select.style.margin = '-1px';
40
+ select.style.overflow = 'hidden';
41
+ select.style.clip = 'rect(0,0,0,0)';
42
+ select.style.whiteSpace = 'nowrap';
43
+ select.style.border = '0';
44
+ for (var option of options) {
45
+ select.options.add(new Option(option, option, true, true));
46
+ }
47
+ form.appendChild(select);
48
+ return select;
18
49
  }
19
- function getEventTarget(formId, name) {
20
- var element = getFieldElement(formId, name);
21
- if (element) {
22
- return element;
50
+ function isDummySelect(element) {
51
+ return element.dataset.conform === 'true';
52
+ }
53
+ function updateFieldValue(element, value) {
54
+ if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
55
+ element.checked = Array.isArray(value) ? value.includes(element.value) : element.value === value;
56
+ } else if (element instanceof HTMLSelectElement && element.multiple) {
57
+ var selectedValue = Array.isArray(value) ? [...value] : [value];
58
+ for (var option of element.options) {
59
+ var index = selectedValue.indexOf(option.value);
60
+ var selected = index > -1;
61
+
62
+ // Update the selected state of the option
63
+ option.selected = selected;
64
+ // Remove the option from the selected array
65
+ if (selected) {
66
+ selectedValue.splice(index, 1);
67
+ }
68
+ }
69
+
70
+ // Add the remaining options to the select element only if it's a dummy element managed by conform
71
+ if (isDummySelect(element)) {
72
+ for (var _option of selectedValue) {
73
+ element.options.add(new Option(_option, _option, false, true));
74
+ }
75
+ }
76
+ } else if (element.value !== value) {
77
+ // No `change` event will be triggered on React if `element.value` is already updated
78
+
79
+ /**
80
+ * Triggering react custom change event
81
+ * Solution based on dom-testing-library
82
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
83
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
84
+ */
85
+ var {
86
+ set: valueSetter
87
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
88
+ var prototype = Object.getPrototypeOf(element);
89
+ var {
90
+ set: prototypeValueSetter
91
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
92
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
93
+ prototypeValueSetter.call(element, value);
94
+ } else {
95
+ if (valueSetter) {
96
+ valueSetter.call(element, value);
97
+ } else {
98
+ throw new Error('The given element does not have a value setter');
99
+ }
100
+ }
23
101
  }
24
- var form = document.forms.namedItem(formId);
25
- var input = document.createElement('input');
26
- input.type = 'hidden';
27
- input.name = name;
28
- form === null || form === void 0 || form.appendChild(input);
29
- return input;
30
102
  }
31
- function useInputControl(metaOrOptions) {
103
+ function useInputEvent() {
104
+ var ref = useRef(null);
32
105
  var eventDispatched = useRef({
33
106
  change: false,
34
107
  focus: false,
35
108
  blur: false
36
109
  });
37
- var [key, setKey] = useState(metaOrOptions.key);
38
- var [value, setValue] = useState(() => metaOrOptions.initialValue);
39
- if (key !== metaOrOptions.key) {
40
- setValue(metaOrOptions.initialValue);
41
- setKey(metaOrOptions.key);
42
- }
43
110
  useEffect(() => {
44
111
  var createEventListener = listener => {
45
112
  return event => {
46
- var element = getFieldElement(metaOrOptions.formId, metaOrOptions.name, element => element === event.target);
47
- if (element) {
113
+ var element = ref.current;
114
+ if (element && event.target === element) {
48
115
  eventDispatched.current[listener] = true;
49
116
  }
50
117
  };
@@ -60,81 +127,145 @@ function useInputControl(metaOrOptions) {
60
127
  document.removeEventListener('focusin', focusHandler, true);
61
128
  document.removeEventListener('focusout', blurHandler, true);
62
129
  };
63
- }, [metaOrOptions.formId, metaOrOptions.name]);
64
- var handlers = useMemo(() => {
130
+ }, [ref]);
131
+ return useMemo(() => {
65
132
  return {
66
133
  change(value) {
67
134
  if (!eventDispatched.current.change) {
68
- var _element = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
69
135
  eventDispatched.current.change = true;
70
- if (_element instanceof HTMLInputElement && (_element.type === 'checkbox' || _element.type === 'radio')) {
71
- _element.checked = _element.value === value;
72
- } else if (_element.value !== value) {
73
- // No change event will be triggered on React if `element.value` is already updated
136
+ var element = ref.current;
137
+ if (element) {
138
+ updateFieldValue(element, value);
74
139
 
75
- /**
76
- * Triggering react custom change event
77
- * Solution based on dom-testing-library
78
- * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
79
- * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
80
- */
81
- var {
82
- set: valueSetter
83
- } = Object.getOwnPropertyDescriptor(_element, 'value') || {};
84
- var prototype = Object.getPrototypeOf(_element);
85
- var {
86
- set: prototypeValueSetter
87
- } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
88
- if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
89
- prototypeValueSetter.call(_element, value);
90
- } else {
91
- if (valueSetter) {
92
- valueSetter.call(_element, value);
93
- } else {
94
- throw new Error('The given element does not have a value setter');
95
- }
96
- }
140
+ // Dispatch input event with the updated input value
141
+ element.dispatchEvent(new InputEvent('input', {
142
+ bubbles: true
143
+ }));
144
+ // Dispatch change event (necessary for select to update the selected option)
145
+ element.dispatchEvent(new Event('change', {
146
+ bubbles: true
147
+ }));
97
148
  }
98
-
99
- // Dispatch input event with the updated input value
100
- _element.dispatchEvent(new InputEvent('input', {
101
- bubbles: true
102
- }));
103
- // Dispatch change event (necessary for select to update the selected option)
104
- _element.dispatchEvent(new Event('change', {
105
- bubbles: true
106
- }));
107
149
  }
108
- setValue(value);
109
150
  eventDispatched.current.change = false;
110
151
  },
111
152
  focus() {
112
153
  if (!eventDispatched.current.focus) {
113
- var _element2 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
114
154
  eventDispatched.current.focus = true;
115
- _element2.dispatchEvent(new FocusEvent('focusin', {
116
- bubbles: true
117
- }));
118
- _element2.dispatchEvent(new FocusEvent('focus'));
155
+ var element = ref.current;
156
+ if (element) {
157
+ element.dispatchEvent(new FocusEvent('focusin', {
158
+ bubbles: true
159
+ }));
160
+ element.dispatchEvent(new FocusEvent('focus'));
161
+ }
119
162
  }
120
163
  eventDispatched.current.focus = false;
121
164
  },
122
165
  blur() {
123
166
  if (!eventDispatched.current.blur) {
124
- var _element3 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
125
167
  eventDispatched.current.blur = true;
126
- _element3.dispatchEvent(new FocusEvent('focusout', {
127
- bubbles: true
128
- }));
129
- _element3.dispatchEvent(new FocusEvent('blur'));
168
+ var element = ref.current;
169
+ if (element) {
170
+ element.dispatchEvent(new FocusEvent('focusout', {
171
+ bubbles: true
172
+ }));
173
+ element.dispatchEvent(new FocusEvent('blur'));
174
+ }
130
175
  }
131
176
  eventDispatched.current.blur = false;
177
+ },
178
+ register(element) {
179
+ ref.current = element;
132
180
  }
133
181
  };
134
- }, [metaOrOptions.formId, metaOrOptions.name]);
135
- return _objectSpread2(_objectSpread2({}, handlers), {}, {
136
- value
137
- });
182
+ }, []);
183
+ }
184
+ function useInputValue(options) {
185
+ var initializeValue = () => {
186
+ var _options$initialValue;
187
+ if (typeof options.initialValue === 'string') {
188
+ // @ts-expect-error FIXME: To ensure that the type of value is also `string | undefined` if initialValue is not an array
189
+ return options.initialValue;
190
+ }
191
+
192
+ // @ts-expect-error Same as above
193
+ return (_options$initialValue = options.initialValue) === null || _options$initialValue === void 0 ? void 0 : _options$initialValue.map(value => value !== null && value !== void 0 ? value : '');
194
+ };
195
+ var [key, setKey] = useState(options.key);
196
+ var [value, setValue] = useState(initializeValue);
197
+ if (key !== options.key) {
198
+ setValue(initializeValue);
199
+ setKey(options.key);
200
+ }
201
+ return [value, setValue];
202
+ }
203
+ function useControl(meta) {
204
+ var [value, setValue] = useInputValue(meta);
205
+ var {
206
+ register,
207
+ change,
208
+ focus,
209
+ blur
210
+ } = useInputEvent();
211
+ var handleChange = value => {
212
+ setValue(value);
213
+ change(value);
214
+ };
215
+ return {
216
+ register,
217
+ value,
218
+ change: handleChange,
219
+ focus,
220
+ blur
221
+ };
222
+ }
223
+ function useInputControl(meta) {
224
+ var [value, setValue] = useInputValue(meta);
225
+ var initializedRef = useRef(false);
226
+ var {
227
+ register,
228
+ change,
229
+ focus,
230
+ blur
231
+ } = useInputEvent();
232
+ useEffect(() => {
233
+ var form = getFormElement(meta.formId);
234
+ if (!form) {
235
+ // eslint-disable-next-line no-console
236
+ console.warn("useInputControl is unable to find form#".concat(meta.formId, " and identify if a dummy input is required"));
237
+ return;
238
+ }
239
+ var element = getEventTarget(form, meta.name);
240
+ if (!element && typeof value !== 'undefined' && (!Array.isArray(value) || value.length > 0)) {
241
+ element = createDummySelect(form, meta.name, value);
242
+ }
243
+ register(element);
244
+ if (!initializedRef.current) {
245
+ initializedRef.current = true;
246
+ } else {
247
+ change(value !== null && value !== void 0 ? value : '');
248
+ }
249
+ return () => {
250
+ register(null);
251
+ var elements = getFieldElements(form, meta.name);
252
+ for (var _element of elements) {
253
+ if (isDummySelect(_element)) {
254
+ _element.remove();
255
+ }
256
+ }
257
+ };
258
+ }, [meta.formId, meta.name, value, change, register]);
259
+ return {
260
+ value,
261
+ change: setValue,
262
+ focus,
263
+ blur
264
+ };
265
+ }
266
+ function Control(props) {
267
+ var control = useControl(props.meta);
268
+ return props.render(control);
138
269
  }
139
270
 
140
- export { getEventTarget, getFieldElement, useInputControl };
271
+ export { Control, createDummySelect, getEventTarget, getFieldElements, getFormElement, isDummySelect, updateFieldValue, useControl, useInputControl, useInputEvent, useInputValue };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Conform view adapter for react",
4
4
  "homepage": "https://conform.guide",
5
5
  "license": "MIT",
6
- "version": "1.0.0",
6
+ "version": "1.0.2",
7
7
  "main": "index.js",
8
8
  "module": "index.mjs",
9
9
  "types": "index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "url": "https://github.com/edmundhung/conform/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@conform-to/dom": "1.0.0"
33
+ "@conform-to/dom": "1.0.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/react": "^18.2.43",