@conform-to/react 1.0.0-rc.1 → 1.0.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 CHANGED
@@ -8,25 +8,30 @@
8
8
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
9
9
 
10
10
 
11
- Version 1.0.0-rc.0 / License MIT / Copyright (c) 2024 Edmund Hung
11
+ Version 1.0.1 / 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
@@ -3,7 +3,7 @@ import { type FormEvent, type ReactElement, type ReactNode, type MutableRefObjec
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;
@@ -27,9 +27,8 @@ export type FormMetadata<Schema extends Record<string, unknown> = Record<string,
27
27
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
28
28
  noValidate: boolean;
29
29
  };
30
- export type FieldMetadata<Schema = unknown, FormSchema extends Record<string, any> = Record<string, unknown>, FormError = string[]> = Metadata<Schema, FormSchema, FormError> & {
30
+ export type FieldMetadata<Schema = unknown, FormSchema extends Record<string, any> = Record<string, unknown>, FormError = string[]> = Metadata<Schema, FormSchema, FormError> & Constraint & {
31
31
  formId: FormId<FormSchema, FormError>;
32
- constraint?: Constraint;
33
32
  getFieldset: unknown extends Schema ? () => unknown : Schema extends Primitive | Array<any> ? never : () => {
34
33
  [Key in UnionKeyof<Schema>]: FieldMetadata<UnionKeyType<Schema, Key>, FormSchema, FormError>;
35
34
  };
package/context.js CHANGED
@@ -74,9 +74,15 @@ function getMetadata(formId, state, subjectRef) {
74
74
  name,
75
75
  errorId: "".concat(id, "-error"),
76
76
  descriptionId: "".concat(id, "-description"),
77
- initialValue: state.initialValue[name],
78
- value: state.value[name],
79
- errors: state.error[name],
77
+ get initialValue() {
78
+ return state.initialValue[name];
79
+ },
80
+ get value() {
81
+ return state.value[name];
82
+ },
83
+ get errors() {
84
+ return state.error[name];
85
+ },
80
86
  get key() {
81
87
  return state.key[name];
82
88
  },
@@ -134,11 +140,19 @@ function getFieldMetadata(formId, state, subjectRef) {
134
140
  var metadata = getMetadata(formId, state, subjectRef, name);
135
141
  return new Proxy({}, {
136
142
  get(_, key, receiver) {
143
+ var _state$constraint$nam;
137
144
  switch (key) {
138
145
  case 'formId':
139
146
  return formId;
140
- case 'constraint':
141
- return state.constraint[name];
147
+ case 'required':
148
+ case 'minLength':
149
+ case 'maxLength':
150
+ case 'min':
151
+ case 'max':
152
+ case 'pattern':
153
+ case 'step':
154
+ case 'multiple':
155
+ return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key];
142
156
  case 'getFieldList':
143
157
  {
144
158
  return () => {
package/context.mjs CHANGED
@@ -70,9 +70,15 @@ function getMetadata(formId, state, subjectRef) {
70
70
  name,
71
71
  errorId: "".concat(id, "-error"),
72
72
  descriptionId: "".concat(id, "-description"),
73
- initialValue: state.initialValue[name],
74
- value: state.value[name],
75
- errors: state.error[name],
73
+ get initialValue() {
74
+ return state.initialValue[name];
75
+ },
76
+ get value() {
77
+ return state.value[name];
78
+ },
79
+ get errors() {
80
+ return state.error[name];
81
+ },
76
82
  get key() {
77
83
  return state.key[name];
78
84
  },
@@ -130,11 +136,19 @@ function getFieldMetadata(formId, state, subjectRef) {
130
136
  var metadata = getMetadata(formId, state, subjectRef, name);
131
137
  return new Proxy({}, {
132
138
  get(_, key, receiver) {
139
+ var _state$constraint$nam;
133
140
  switch (key) {
134
141
  case 'formId':
135
142
  return formId;
136
- case 'constraint':
137
- return state.constraint[name];
143
+ case 'required':
144
+ case 'minLength':
145
+ case 'maxLength':
146
+ case 'min':
147
+ case 'max':
148
+ case 'pattern':
149
+ case 'step':
150
+ case 'multiple':
151
+ return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key];
138
152
  case 'getFieldList':
139
153
  {
140
154
  return () => {
package/helpers.js CHANGED
@@ -72,10 +72,9 @@ function getFieldsetProps(metadata, options) {
72
72
  * including `key`, `id`, `name`, `form`, `required`, `autoFocus`, `aria-invalid` and `aria-describedby`.
73
73
  */
74
74
  function getFormControlProps(metadata, options) {
75
- var _metadata$constraint;
76
75
  return simplify(_rollupPluginBabelHelpers.objectSpread2({
77
76
  key: metadata.key,
78
- required: ((_metadata$constraint = metadata.constraint) === null || _metadata$constraint === void 0 ? void 0 : _metadata$constraint.required) || undefined
77
+ required: metadata.required || undefined
79
78
  }, getFieldsetProps(metadata, options)));
80
79
  }
81
80
 
@@ -100,16 +99,15 @@ function getFormControlProps(metadata, options) {
100
99
  * ```
101
100
  */
102
101
  function getInputProps(metadata, options) {
103
- var _metadata$constraint2, _metadata$constraint3, _metadata$constraint4, _metadata$constraint5, _metadata$constraint6, _metadata$constraint7, _metadata$constraint8;
104
102
  var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, {
105
103
  type: options.type,
106
- minLength: (_metadata$constraint2 = metadata.constraint) === null || _metadata$constraint2 === void 0 ? void 0 : _metadata$constraint2.minLength,
107
- maxLength: (_metadata$constraint3 = metadata.constraint) === null || _metadata$constraint3 === void 0 ? void 0 : _metadata$constraint3.maxLength,
108
- min: (_metadata$constraint4 = metadata.constraint) === null || _metadata$constraint4 === void 0 ? void 0 : _metadata$constraint4.min,
109
- max: (_metadata$constraint5 = metadata.constraint) === null || _metadata$constraint5 === void 0 ? void 0 : _metadata$constraint5.max,
110
- step: (_metadata$constraint6 = metadata.constraint) === null || _metadata$constraint6 === void 0 ? void 0 : _metadata$constraint6.step,
111
- pattern: (_metadata$constraint7 = metadata.constraint) === null || _metadata$constraint7 === void 0 ? void 0 : _metadata$constraint7.pattern,
112
- multiple: (_metadata$constraint8 = metadata.constraint) === null || _metadata$constraint8 === void 0 ? void 0 : _metadata$constraint8.multiple
104
+ minLength: metadata.minLength,
105
+ maxLength: metadata.maxLength,
106
+ min: metadata.min,
107
+ max: metadata.max,
108
+ step: metadata.step,
109
+ pattern: metadata.pattern,
110
+ multiple: metadata.multiple
113
111
  });
114
112
  if (typeof options.value === 'undefined' || options.value) {
115
113
  if (options.type === 'checkbox' || options.type === 'radio') {
@@ -138,10 +136,9 @@ function getInputProps(metadata, options) {
138
136
  * ```
139
137
  */
140
138
  function getSelectProps(metadata) {
141
- var _metadata$constraint9;
142
139
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
143
140
  var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, {
144
- multiple: (_metadata$constraint9 = metadata.constraint) === null || _metadata$constraint9 === void 0 ? void 0 : _metadata$constraint9.multiple
141
+ multiple: metadata.multiple
145
142
  });
146
143
  if (typeof options.value === 'undefined' || options.value) {
147
144
  props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : metadata.initialValue;
@@ -165,11 +162,10 @@ function getSelectProps(metadata) {
165
162
  * ```
166
163
  */
167
164
  function getTextareaProps(metadata) {
168
- var _metadata$constraint10, _metadata$constraint11;
169
165
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
170
166
  var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, {
171
- minLength: (_metadata$constraint10 = metadata.constraint) === null || _metadata$constraint10 === void 0 ? void 0 : _metadata$constraint10.minLength,
172
- maxLength: (_metadata$constraint11 = metadata.constraint) === null || _metadata$constraint11 === void 0 ? void 0 : _metadata$constraint11.maxLength
167
+ minLength: metadata.minLength,
168
+ maxLength: metadata.maxLength
173
169
  });
174
170
  if (typeof options.value === 'undefined' || options.value) {
175
171
  props.defaultValue = metadata.initialValue;
@@ -199,7 +195,7 @@ function getTextareaProps(metadata) {
199
195
  */
200
196
  function getCollectionProps(metadata, options) {
201
197
  return options.options.map(value => {
202
- var _metadata$key, _metadata$constraint12;
198
+ var _metadata$key;
203
199
  return simplify(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, {
204
200
  key: "".concat((_metadata$key = metadata.key) !== null && _metadata$key !== void 0 ? _metadata$key : '').concat(value),
205
201
  id: "".concat(metadata.id, "-").concat(value),
@@ -209,7 +205,7 @@ function getCollectionProps(metadata, options) {
209
205
  // The required attribute doesn't make sense for checkbox group
210
206
  // As it would require all checkboxes to be checked instead of at least one
211
207
  // It is overriden with `undefiend` so it could be cleaned up properly
212
- required: options.type === 'checkbox' ? undefined : (_metadata$constraint12 = metadata.constraint) === null || _metadata$constraint12 === void 0 ? void 0 : _metadata$constraint12.required
208
+ required: options.type === 'checkbox' ? undefined : metadata.required
213
209
  }));
214
210
  });
215
211
  }
package/helpers.mjs CHANGED
@@ -68,10 +68,9 @@ function getFieldsetProps(metadata, options) {
68
68
  * including `key`, `id`, `name`, `form`, `required`, `autoFocus`, `aria-invalid` and `aria-describedby`.
69
69
  */
70
70
  function getFormControlProps(metadata, options) {
71
- var _metadata$constraint;
72
71
  return simplify(_objectSpread2({
73
72
  key: metadata.key,
74
- required: ((_metadata$constraint = metadata.constraint) === null || _metadata$constraint === void 0 ? void 0 : _metadata$constraint.required) || undefined
73
+ required: metadata.required || undefined
75
74
  }, getFieldsetProps(metadata, options)));
76
75
  }
77
76
 
@@ -96,16 +95,15 @@ function getFormControlProps(metadata, options) {
96
95
  * ```
97
96
  */
98
97
  function getInputProps(metadata, options) {
99
- var _metadata$constraint2, _metadata$constraint3, _metadata$constraint4, _metadata$constraint5, _metadata$constraint6, _metadata$constraint7, _metadata$constraint8;
100
98
  var props = _objectSpread2(_objectSpread2({}, getFormControlProps(metadata, options)), {}, {
101
99
  type: options.type,
102
- minLength: (_metadata$constraint2 = metadata.constraint) === null || _metadata$constraint2 === void 0 ? void 0 : _metadata$constraint2.minLength,
103
- maxLength: (_metadata$constraint3 = metadata.constraint) === null || _metadata$constraint3 === void 0 ? void 0 : _metadata$constraint3.maxLength,
104
- min: (_metadata$constraint4 = metadata.constraint) === null || _metadata$constraint4 === void 0 ? void 0 : _metadata$constraint4.min,
105
- max: (_metadata$constraint5 = metadata.constraint) === null || _metadata$constraint5 === void 0 ? void 0 : _metadata$constraint5.max,
106
- step: (_metadata$constraint6 = metadata.constraint) === null || _metadata$constraint6 === void 0 ? void 0 : _metadata$constraint6.step,
107
- pattern: (_metadata$constraint7 = metadata.constraint) === null || _metadata$constraint7 === void 0 ? void 0 : _metadata$constraint7.pattern,
108
- multiple: (_metadata$constraint8 = metadata.constraint) === null || _metadata$constraint8 === void 0 ? void 0 : _metadata$constraint8.multiple
100
+ minLength: metadata.minLength,
101
+ maxLength: metadata.maxLength,
102
+ min: metadata.min,
103
+ max: metadata.max,
104
+ step: metadata.step,
105
+ pattern: metadata.pattern,
106
+ multiple: metadata.multiple
109
107
  });
110
108
  if (typeof options.value === 'undefined' || options.value) {
111
109
  if (options.type === 'checkbox' || options.type === 'radio') {
@@ -134,10 +132,9 @@ function getInputProps(metadata, options) {
134
132
  * ```
135
133
  */
136
134
  function getSelectProps(metadata) {
137
- var _metadata$constraint9;
138
135
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
139
136
  var props = _objectSpread2(_objectSpread2({}, getFormControlProps(metadata, options)), {}, {
140
- multiple: (_metadata$constraint9 = metadata.constraint) === null || _metadata$constraint9 === void 0 ? void 0 : _metadata$constraint9.multiple
137
+ multiple: metadata.multiple
141
138
  });
142
139
  if (typeof options.value === 'undefined' || options.value) {
143
140
  props.defaultValue = Array.isArray(metadata.initialValue) ? metadata.initialValue.map(item => "".concat(item !== null && item !== void 0 ? item : '')) : metadata.initialValue;
@@ -161,11 +158,10 @@ function getSelectProps(metadata) {
161
158
  * ```
162
159
  */
163
160
  function getTextareaProps(metadata) {
164
- var _metadata$constraint10, _metadata$constraint11;
165
161
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
166
162
  var props = _objectSpread2(_objectSpread2({}, getFormControlProps(metadata, options)), {}, {
167
- minLength: (_metadata$constraint10 = metadata.constraint) === null || _metadata$constraint10 === void 0 ? void 0 : _metadata$constraint10.minLength,
168
- maxLength: (_metadata$constraint11 = metadata.constraint) === null || _metadata$constraint11 === void 0 ? void 0 : _metadata$constraint11.maxLength
163
+ minLength: metadata.minLength,
164
+ maxLength: metadata.maxLength
169
165
  });
170
166
  if (typeof options.value === 'undefined' || options.value) {
171
167
  props.defaultValue = metadata.initialValue;
@@ -195,7 +191,7 @@ function getTextareaProps(metadata) {
195
191
  */
196
192
  function getCollectionProps(metadata, options) {
197
193
  return options.options.map(value => {
198
- var _metadata$key, _metadata$constraint12;
194
+ var _metadata$key;
199
195
  return simplify(_objectSpread2(_objectSpread2({}, getFormControlProps(metadata, options)), {}, {
200
196
  key: "".concat((_metadata$key = metadata.key) !== null && _metadata$key !== void 0 ? _metadata$key : '').concat(value),
201
197
  id: "".concat(metadata.id, "-").concat(value),
@@ -205,7 +201,7 @@ function getCollectionProps(metadata, options) {
205
201
  // The required attribute doesn't make sense for checkbox group
206
202
  // As it would require all checkboxes to be checked instead of at least one
207
203
  // It is overriden with `undefiend` so it could be cleaned up properly
208
- required: options.type === 'checkbox' ? undefined : (_metadata$constraint12 = metadata.constraint) === null || _metadata$constraint12 === void 0 ? void 0 : _metadata$constraint12.required
204
+ required: options.type === 'checkbox' ? undefined : metadata.required
209
205
  }));
210
206
  });
211
207
  }
package/integrations.d.ts CHANGED
@@ -1,17 +1,19 @@
1
- import { type FieldElement } from '@conform-to/dom';
2
1
  import { type Key } from 'react';
3
- export type InputControl = {
4
- value: string | undefined;
5
- change: (value: string) => void;
2
+ export type InputControl<Value> = {
3
+ value: Value | undefined;
4
+ change: (value: Value) => void;
6
5
  focus: () => void;
7
6
  blur: () => void;
8
7
  };
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 = {
8
+ export declare function getFormElement(formId: string): HTMLFormElement;
9
+ export declare function getFieldElements(form: HTMLFormElement, name: string): Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
10
+ export declare function getEventTarget(form: HTMLFormElement, name: string): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
11
+ export declare function createDummySelect(form: HTMLFormElement, name: string, value?: string | string[] | undefined): HTMLSelectElement;
12
+ export declare function isDummySelect(element: HTMLElement): element is HTMLSelectElement;
13
+ export declare function updateFieldValue(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, value: string | string[]): void;
14
+ export declare function useInputControl<Value>(metaOrOptions: {
12
15
  key?: Key | null | undefined;
13
16
  name: string;
14
17
  formId: string;
15
- initialValue?: string | undefined;
16
- };
17
- export declare function useInputControl(metaOrOptions: InputControlOptions): InputControl;
18
+ initialValue?: Value | undefined;
19
+ }): InputControl<Value extends string ? string : string | string[]>;
package/integrations.js CHANGED
@@ -3,52 +3,141 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
- var dom = require('@conform-to/dom');
7
6
  var react = require('react');
8
7
 
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;
18
- }
19
- }
8
+ function getFormElement(formId) {
9
+ var element = document.forms.namedItem(formId);
10
+ if (!element) {
11
+ throw new Error('Form not found');
12
+ }
13
+ return element;
14
+ }
15
+ function getFieldElements(form, name) {
16
+ var field = form.elements.namedItem(name);
17
+ var elements = !field ? [] : field instanceof Element ? [field] : Array.from(field.values());
18
+ return elements.filter(element => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement);
19
+ }
20
+ function getEventTarget(form, name) {
21
+ var _elements$;
22
+ var elements = getFieldElements(form, name);
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));
20
46
  }
21
- return null;
47
+ form.appendChild(select);
48
+ return select;
22
49
  }
23
- function getEventTarget(formId, name) {
24
- var element = getFieldElement(formId, name);
25
- if (element) {
26
- 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 = 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
+ }
27
101
  }
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
102
  }
35
103
  function useInputControl(metaOrOptions) {
104
+ // If the initialValue is an array, it must be a string array without undefined values
105
+ // as there is no way to skip an entry in a multiple select when they all share the same name
106
+ var inputInitialValue = metaOrOptions.initialValue;
36
107
  var eventDispatched = react.useRef({
37
108
  change: false,
38
109
  focus: false,
39
110
  blur: false
40
111
  });
41
112
  var [key, setKey] = react.useState(metaOrOptions.key);
42
- var [value, setValue] = react.useState(() => metaOrOptions.initialValue);
113
+ var [initialValue, setInitialValue] = react.useState(inputInitialValue);
114
+ var [value, setValue] = react.useState(inputInitialValue);
43
115
  if (key !== metaOrOptions.key) {
44
- setValue(metaOrOptions.initialValue);
116
+ setValue(inputInitialValue);
117
+ setInitialValue(inputInitialValue);
45
118
  setKey(metaOrOptions.key);
46
119
  }
120
+ react.useEffect(() => {
121
+ var form = getFormElement(metaOrOptions.formId);
122
+ if (getEventTarget(form, metaOrOptions.name)) {
123
+ return;
124
+ }
125
+ createDummySelect(form, metaOrOptions.name, initialValue);
126
+ return () => {
127
+ var elements = getFieldElements(form, metaOrOptions.name);
128
+ for (var element of elements) {
129
+ if (isDummySelect(element)) {
130
+ element.remove();
131
+ }
132
+ }
133
+ };
134
+ }, [metaOrOptions.formId, metaOrOptions.name, initialValue]);
47
135
  react.useEffect(() => {
48
136
  var createEventListener = listener => {
49
137
  return event => {
50
- var element = getFieldElement(metaOrOptions.formId, metaOrOptions.name, element => element === event.target);
51
- if (element) {
138
+ var _element$form;
139
+ var element = event.target;
140
+ if ((element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) && element.name === metaOrOptions.name && ((_element$form = element.form) === null || _element$form === void 0 ? void 0 : _element$form.id) === metaOrOptions.formId) {
52
141
  eventDispatched.current[listener] = true;
53
142
  }
54
143
  };
@@ -69,68 +158,50 @@ function useInputControl(metaOrOptions) {
69
158
  return {
70
159
  change(value) {
71
160
  if (!eventDispatched.current.change) {
72
- var _element = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
73
161
  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
162
+ var form = getFormElement(metaOrOptions.formId);
163
+ var element = getEventTarget(form, metaOrOptions.name);
164
+ if (element) {
165
+ updateFieldValue(element, value);
78
166
 
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
- }
167
+ // Dispatch input event with the updated input value
168
+ element.dispatchEvent(new InputEvent('input', {
169
+ bubbles: true
170
+ }));
171
+ // Dispatch change event (necessary for select to update the selected option)
172
+ element.dispatchEvent(new Event('change', {
173
+ bubbles: true
174
+ }));
101
175
  }
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
176
  }
112
177
  setValue(value);
113
178
  eventDispatched.current.change = false;
114
179
  },
115
180
  focus() {
116
181
  if (!eventDispatched.current.focus) {
117
- var _element2 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
118
182
  eventDispatched.current.focus = true;
119
- _element2.dispatchEvent(new FocusEvent('focusin', {
120
- bubbles: true
121
- }));
122
- _element2.dispatchEvent(new FocusEvent('focus'));
183
+ var form = getFormElement(metaOrOptions.formId);
184
+ var element = getEventTarget(form, metaOrOptions.name);
185
+ if (element) {
186
+ element.dispatchEvent(new FocusEvent('focusin', {
187
+ bubbles: true
188
+ }));
189
+ element.dispatchEvent(new FocusEvent('focus'));
190
+ }
123
191
  }
124
192
  eventDispatched.current.focus = false;
125
193
  },
126
194
  blur() {
127
195
  if (!eventDispatched.current.blur) {
128
- var _element3 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
129
196
  eventDispatched.current.blur = true;
130
- _element3.dispatchEvent(new FocusEvent('focusout', {
131
- bubbles: true
132
- }));
133
- _element3.dispatchEvent(new FocusEvent('blur'));
197
+ var form = getFormElement(metaOrOptions.formId);
198
+ var element = getEventTarget(form, metaOrOptions.name);
199
+ if (element) {
200
+ element.dispatchEvent(new FocusEvent('focusout', {
201
+ bubbles: true
202
+ }));
203
+ element.dispatchEvent(new FocusEvent('blur'));
204
+ }
134
205
  }
135
206
  eventDispatched.current.blur = false;
136
207
  }
@@ -141,6 +212,10 @@ function useInputControl(metaOrOptions) {
141
212
  });
142
213
  }
143
214
 
215
+ exports.createDummySelect = createDummySelect;
144
216
  exports.getEventTarget = getEventTarget;
145
- exports.getFieldElement = getFieldElement;
217
+ exports.getFieldElements = getFieldElements;
218
+ exports.getFormElement = getFormElement;
219
+ exports.isDummySelect = isDummySelect;
220
+ exports.updateFieldValue = updateFieldValue;
146
221
  exports.useInputControl = useInputControl;
package/integrations.mjs CHANGED
@@ -1,50 +1,139 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
2
- import { isFieldElement } from '@conform-to/dom';
3
2
  import { useRef, useState, useEffect, useMemo } from 'react';
4
3
 
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;
14
- }
15
- }
4
+ function getFormElement(formId) {
5
+ var element = document.forms.namedItem(formId);
6
+ if (!element) {
7
+ throw new Error('Form not found');
8
+ }
9
+ return element;
10
+ }
11
+ function getFieldElements(form, name) {
12
+ var field = form.elements.namedItem(name);
13
+ var elements = !field ? [] : field instanceof Element ? [field] : Array.from(field.values());
14
+ return elements.filter(element => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement);
15
+ }
16
+ function getEventTarget(form, name) {
17
+ var _elements$;
18
+ var elements = getFieldElements(form, name);
19
+ return (_elements$ = elements[0]) !== null && _elements$ !== void 0 ? _elements$ : null;
20
+ }
21
+ function createDummySelect(form, name, value) {
22
+ var select = document.createElement('select');
23
+ var options = typeof value === 'string' ? [value] : value !== null && value !== void 0 ? value : [];
24
+ select.name = name;
25
+ select.multiple = true;
26
+ select.dataset.conform = 'true';
27
+
28
+ // To make sure the input is hidden but still focusable
29
+ select.setAttribute('aria-hidden', 'true');
30
+ select.tabIndex = -1;
31
+ select.style.position = 'absolute';
32
+ select.style.width = '1px';
33
+ select.style.height = '1px';
34
+ select.style.padding = '0';
35
+ select.style.margin = '-1px';
36
+ select.style.overflow = 'hidden';
37
+ select.style.clip = 'rect(0,0,0,0)';
38
+ select.style.whiteSpace = 'nowrap';
39
+ select.style.border = '0';
40
+ for (var option of options) {
41
+ select.options.add(new Option(option, option, true, true));
16
42
  }
17
- return null;
43
+ form.appendChild(select);
44
+ return select;
18
45
  }
19
- function getEventTarget(formId, name) {
20
- var element = getFieldElement(formId, name);
21
- if (element) {
22
- return element;
46
+ function isDummySelect(element) {
47
+ return element.dataset.conform === 'true';
48
+ }
49
+ function updateFieldValue(element, value) {
50
+ if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
51
+ element.checked = element.value === value;
52
+ } else if (element instanceof HTMLSelectElement && element.multiple) {
53
+ var selectedValue = Array.isArray(value) ? [...value] : [value];
54
+ for (var option of element.options) {
55
+ var index = selectedValue.indexOf(option.value);
56
+ var selected = index > -1;
57
+
58
+ // Update the selected state of the option
59
+ option.selected = selected;
60
+ // Remove the option from the selected array
61
+ if (selected) {
62
+ selectedValue.splice(index, 1);
63
+ }
64
+ }
65
+
66
+ // Add the remaining options to the select element only if it's a dummy element managed by conform
67
+ if (isDummySelect(element)) {
68
+ for (var _option of selectedValue) {
69
+ element.options.add(new Option(_option, _option, false, true));
70
+ }
71
+ }
72
+ } else if (element.value !== value) {
73
+ // No `change` event will be triggered on React if `element.value` is already updated
74
+
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
+ }
23
97
  }
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
98
  }
31
99
  function useInputControl(metaOrOptions) {
100
+ // If the initialValue is an array, it must be a string array without undefined values
101
+ // as there is no way to skip an entry in a multiple select when they all share the same name
102
+ var inputInitialValue = metaOrOptions.initialValue;
32
103
  var eventDispatched = useRef({
33
104
  change: false,
34
105
  focus: false,
35
106
  blur: false
36
107
  });
37
108
  var [key, setKey] = useState(metaOrOptions.key);
38
- var [value, setValue] = useState(() => metaOrOptions.initialValue);
109
+ var [initialValue, setInitialValue] = useState(inputInitialValue);
110
+ var [value, setValue] = useState(inputInitialValue);
39
111
  if (key !== metaOrOptions.key) {
40
- setValue(metaOrOptions.initialValue);
112
+ setValue(inputInitialValue);
113
+ setInitialValue(inputInitialValue);
41
114
  setKey(metaOrOptions.key);
42
115
  }
116
+ useEffect(() => {
117
+ var form = getFormElement(metaOrOptions.formId);
118
+ if (getEventTarget(form, metaOrOptions.name)) {
119
+ return;
120
+ }
121
+ createDummySelect(form, metaOrOptions.name, initialValue);
122
+ return () => {
123
+ var elements = getFieldElements(form, metaOrOptions.name);
124
+ for (var element of elements) {
125
+ if (isDummySelect(element)) {
126
+ element.remove();
127
+ }
128
+ }
129
+ };
130
+ }, [metaOrOptions.formId, metaOrOptions.name, initialValue]);
43
131
  useEffect(() => {
44
132
  var createEventListener = listener => {
45
133
  return event => {
46
- var element = getFieldElement(metaOrOptions.formId, metaOrOptions.name, element => element === event.target);
47
- if (element) {
134
+ var _element$form;
135
+ var element = event.target;
136
+ if ((element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) && element.name === metaOrOptions.name && ((_element$form = element.form) === null || _element$form === void 0 ? void 0 : _element$form.id) === metaOrOptions.formId) {
48
137
  eventDispatched.current[listener] = true;
49
138
  }
50
139
  };
@@ -65,68 +154,50 @@ function useInputControl(metaOrOptions) {
65
154
  return {
66
155
  change(value) {
67
156
  if (!eventDispatched.current.change) {
68
- var _element = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
69
157
  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
158
+ var form = getFormElement(metaOrOptions.formId);
159
+ var element = getEventTarget(form, metaOrOptions.name);
160
+ if (element) {
161
+ updateFieldValue(element, value);
74
162
 
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
- }
163
+ // Dispatch input event with the updated input value
164
+ element.dispatchEvent(new InputEvent('input', {
165
+ bubbles: true
166
+ }));
167
+ // Dispatch change event (necessary for select to update the selected option)
168
+ element.dispatchEvent(new Event('change', {
169
+ bubbles: true
170
+ }));
97
171
  }
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
172
  }
108
173
  setValue(value);
109
174
  eventDispatched.current.change = false;
110
175
  },
111
176
  focus() {
112
177
  if (!eventDispatched.current.focus) {
113
- var _element2 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
114
178
  eventDispatched.current.focus = true;
115
- _element2.dispatchEvent(new FocusEvent('focusin', {
116
- bubbles: true
117
- }));
118
- _element2.dispatchEvent(new FocusEvent('focus'));
179
+ var form = getFormElement(metaOrOptions.formId);
180
+ var element = getEventTarget(form, metaOrOptions.name);
181
+ if (element) {
182
+ element.dispatchEvent(new FocusEvent('focusin', {
183
+ bubbles: true
184
+ }));
185
+ element.dispatchEvent(new FocusEvent('focus'));
186
+ }
119
187
  }
120
188
  eventDispatched.current.focus = false;
121
189
  },
122
190
  blur() {
123
191
  if (!eventDispatched.current.blur) {
124
- var _element3 = getEventTarget(metaOrOptions.formId, metaOrOptions.name);
125
192
  eventDispatched.current.blur = true;
126
- _element3.dispatchEvent(new FocusEvent('focusout', {
127
- bubbles: true
128
- }));
129
- _element3.dispatchEvent(new FocusEvent('blur'));
193
+ var form = getFormElement(metaOrOptions.formId);
194
+ var element = getEventTarget(form, metaOrOptions.name);
195
+ if (element) {
196
+ element.dispatchEvent(new FocusEvent('focusout', {
197
+ bubbles: true
198
+ }));
199
+ element.dispatchEvent(new FocusEvent('blur'));
200
+ }
130
201
  }
131
202
  eventDispatched.current.blur = false;
132
203
  }
@@ -137,4 +208,4 @@ function useInputControl(metaOrOptions) {
137
208
  });
138
209
  }
139
210
 
140
- export { getEventTarget, getFieldElement, useInputControl };
211
+ export { createDummySelect, getEventTarget, getFieldElements, getFormElement, isDummySelect, updateFieldValue, useInputControl };
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-rc.1",
6
+ "version": "1.0.1",
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-rc.1"
33
+ "@conform-to/dom": "1.0.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/react": "^18.2.43",