@conform-to/react 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ ```
2
+ ███████╗ ██████╗ ███╗ ██╗ ████████╗ ██████╗ ███████╗ ███╗ ███╗
3
+ ██╔═════╝ ██╔═══██╗ ████╗ ██║ ██╔═════╝ ██╔═══██╗ ██╔═══██╗ ████████║
4
+ ██║ ██║ ██║ ██╔██╗██║ ███████╗ ██║ ██║ ███████╔╝ ██╔██╔██║
5
+ ██║ ██║ ██║ ██║╚████║ ██╔════╝ ██║ ██║ ██╔═══██╗ ██║╚═╝██║
6
+ ╚███████╗ ╚██████╔╝ ██║ ╚███║ ██║ ╚██████╔╝ ██║ ██║ ██║ ██║
7
+ ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
+ ```
9
+
10
+ Version 1.5.0 / License MIT / Copyright (c) 2024 Edmund Hung
11
+
12
+ 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.
13
+
14
+ # Getting Started
15
+
16
+ Check out the overview and tutorial at our website https://conform.guide
17
+
18
+ # Features
19
+
20
+ - Progressive enhancement first APIs
21
+ - Type-safe field inference
22
+ - Fine-grained subscription
23
+ - Built-in accessibility helpers
24
+ - Automatic type coercion with Zod
25
+
26
+ # Documentation
27
+
28
+ - Validation: https://conform.guide/validation
29
+ - Nested object and Array: https://conform.guide/complex-structures
30
+ - UI Integrations: https://conform.guide/integration/ui-libraries
31
+ - Intent button: https://conform.guide/intent-button
32
+ - Accessibility Guide: https://conform.guide/accessibility
33
+
34
+ # Support
35
+
36
+ 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.js CHANGED
@@ -56,7 +56,7 @@ function useSubjectRef() {
56
56
  return subjectRef;
57
57
  }
58
58
  function updateSubjectRef(ref, subject, scope, name) {
59
- if (subject === 'status' || subject === 'formId') {
59
+ if (subject === 'status' || subject === 'formId' || subject === 'pendingIntents') {
60
60
  ref.current[subject] = true;
61
61
  } else if (typeof scope !== 'undefined' && typeof name !== 'undefined') {
62
62
  var _ref$current$subject$, _ref$current$subject;
package/context.mjs CHANGED
@@ -52,7 +52,7 @@ function useSubjectRef() {
52
52
  return subjectRef;
53
53
  }
54
54
  function updateSubjectRef(ref, subject, scope, name) {
55
- if (subject === 'status' || subject === 'formId') {
55
+ if (subject === 'status' || subject === 'formId' || subject === 'pendingIntents') {
56
56
  ref.current[subject] = true;
57
57
  } else if (typeof scope !== 'undefined' && typeof name !== 'undefined') {
58
58
  var _ref$current$subject$, _ref$current$subject;
package/helpers.js CHANGED
@@ -73,7 +73,7 @@ function getFieldsetProps(metadata, options) {
73
73
  */
74
74
  function getFormControlProps(metadata, options) {
75
75
  return simplify(_rollupPluginBabelHelpers.objectSpread2({
76
- key: metadata.key,
76
+ key: undefined,
77
77
  required: metadata.required || undefined
78
78
  }, getFieldsetProps(metadata, options)));
79
79
  }
package/helpers.mjs CHANGED
@@ -69,7 +69,7 @@ function getFieldsetProps(metadata, options) {
69
69
  */
70
70
  function getFormControlProps(metadata, options) {
71
71
  return simplify(_objectSpread2({
72
- key: metadata.key,
72
+ key: undefined,
73
73
  required: metadata.required || undefined
74
74
  }, getFieldsetProps(metadata, options)));
75
75
  }
package/hooks.js CHANGED
@@ -56,10 +56,15 @@ function useForm(options) {
56
56
  formId
57
57
  }));
58
58
  });
59
- var subjectRef = context.useSubjectRef();
59
+ var subjectRef = context.useSubjectRef({
60
+ pendingIntents: true
61
+ });
60
62
  var stateSnapshot = context.useFormState(context$1, subjectRef);
61
63
  var noValidate = useNoValidate(options.defaultNoValidate);
62
64
  var form = context.getFormMetadata(context$1, subjectRef, stateSnapshot, noValidate);
65
+ react.useEffect(() => {
66
+ context$1.runSideEffect(stateSnapshot.pendingIntents);
67
+ }, [context$1, stateSnapshot.pendingIntents]);
63
68
  return [form, form.getFieldset()];
64
69
  }
65
70
  function useFormMetadata(formId) {
package/hooks.mjs CHANGED
@@ -52,10 +52,15 @@ function useForm(options) {
52
52
  formId
53
53
  }));
54
54
  });
55
- var subjectRef = useSubjectRef();
55
+ var subjectRef = useSubjectRef({
56
+ pendingIntents: true
57
+ });
56
58
  var stateSnapshot = useFormState(context, subjectRef);
57
59
  var noValidate = useNoValidate(options.defaultNoValidate);
58
60
  var form = getFormMetadata(context, subjectRef, stateSnapshot, noValidate);
61
+ useEffect(() => {
62
+ context.runSideEffect(stateSnapshot.pendingIntents);
63
+ }, [context, stateSnapshot.pendingIntents]);
59
64
  return [form, form.getFieldset()];
60
65
  }
61
66
  function useFormMetadata(formId) {
package/integrations.d.ts CHANGED
@@ -4,8 +4,8 @@ export declare function getFieldElements(form: HTMLFormElement | null, name: str
4
4
  export declare function getEventTarget(form: HTMLFormElement | null, name: string, value?: string | string[]): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
5
5
  export declare function createDummySelect(form: HTMLFormElement, name: string, value?: string | string[] | undefined): HTMLSelectElement;
6
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(): {
7
+ export declare function getInputValue(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): string | string[] | null;
8
+ export declare function useInputEvent(onUpdate: React.Dispatch<React.SetStateAction<string | string[] | undefined>>): {
9
9
  change(value: string | string[]): void;
10
10
  focus(): void;
11
11
  blur(): void;
package/integrations.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var dom = require('@conform-to/dom');
5
6
  var react = require('react');
6
7
 
7
8
  function getFormElement(formId) {
@@ -54,58 +55,20 @@ function createDummySelect(form, name, value) {
54
55
  function isDummySelect(element) {
55
56
  return element.dataset.conform === 'true';
56
57
  }
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
- }
58
+ function getInputValue(element) {
59
+ if (element instanceof HTMLSelectElement) {
60
+ var _value$;
61
+ var _value = Array.from(element.selectedOptions).map(option => option.value);
62
+ return element.multiple ? _value : (_value$ = _value[0]) !== null && _value$ !== void 0 ? _value$ : null;
105
63
  }
64
+ if (element instanceof HTMLInputElement && (element.type === 'radio' || element.type === 'checkbox')) {
65
+ return element.checked ? element.value : null;
66
+ }
67
+ return element.value;
106
68
  }
107
- function useInputEvent() {
69
+ function useInputEvent(onUpdate) {
108
70
  var ref = react.useRef(null);
71
+ var observerRef = react.useRef(null);
109
72
  var eventDispatched = react.useRef({
110
73
  change: false,
111
74
  focus: false,
@@ -139,7 +102,9 @@ function useInputEvent() {
139
102
  eventDispatched.current.change = true;
140
103
  var element = ref.current;
141
104
  if (element) {
142
- updateFieldValue(element, value);
105
+ dom.unstable_updateFieldValue(element, {
106
+ value
107
+ });
143
108
 
144
109
  // Dispatch input event with the updated input value
145
110
  element.dispatchEvent(new InputEvent('input', {
@@ -181,9 +146,39 @@ function useInputEvent() {
181
146
  },
182
147
  register(element) {
183
148
  ref.current = element;
149
+ if (observerRef.current) {
150
+ observerRef.current.disconnect();
151
+ observerRef.current = null;
152
+ }
153
+ if (!element) {
154
+ return;
155
+ }
156
+ observerRef.current = new MutationObserver(mutations => {
157
+ var _loop = function _loop() {
158
+ if (mutation.type === 'attributes') {
159
+ var _getInputValue;
160
+ var nextValue = (_getInputValue = getInputValue(element)) !== null && _getInputValue !== void 0 ? _getInputValue : undefined;
161
+ onUpdate(prevValue => {
162
+ if (nextValue === prevValue ||
163
+ // If the value is an array, check if the current value is the same as the new value
164
+ JSON.stringify(prevValue) === JSON.stringify(nextValue)) {
165
+ return prevValue;
166
+ }
167
+ return nextValue;
168
+ });
169
+ }
170
+ };
171
+ for (var mutation of mutations) {
172
+ _loop();
173
+ }
174
+ });
175
+ observerRef.current.observe(element, {
176
+ attributes: true,
177
+ attributeFilter: ['data-conform']
178
+ });
184
179
  }
185
180
  };
186
- }, []);
181
+ }, [onUpdate]);
187
182
  }
188
183
  function useInputValue(options) {
189
184
  var initializeValue = () => {
@@ -211,22 +206,26 @@ function useControl(meta) {
211
206
  change,
212
207
  focus,
213
208
  blur
214
- } = useInputEvent();
209
+ } = useInputEvent(
210
+ // @ts-expect-error We will fix the type when stabilizing the API
211
+ setValue);
215
212
  var handleChange = value => {
216
213
  setValue(value);
217
214
  change(value);
218
215
  };
219
216
  var refCallback = element => {
220
- var _meta$key;
221
217
  register(element);
222
218
  if (!element) {
223
219
  return;
224
220
  }
225
- var prevKey = element.dataset.conform;
226
- var nextKey = "".concat((_meta$key = meta.key) !== null && _meta$key !== void 0 ? _meta$key : '');
227
- if (prevKey !== nextKey) {
228
- element.dataset.conform = nextKey;
229
- updateFieldValue(element, value !== null && value !== void 0 ? value : '');
221
+
222
+ // We were trying to sync the value based on key previously
223
+ // This is now handled mostly by the side effect
224
+ // But we still need to set the initial value for backward compatibility
225
+ if (!element.dataset.conform) {
226
+ dom.unstable_updateFieldValue(element, {
227
+ value
228
+ });
230
229
  }
231
230
  };
232
231
  return {
@@ -245,7 +244,9 @@ function useInputControl(meta) {
245
244
  change,
246
245
  focus,
247
246
  blur
248
- } = useInputEvent();
247
+ } = useInputEvent(
248
+ // @ts-expect-error We will fix the type when stabilizing the API
249
+ setValue);
249
250
  react.useEffect(() => {
250
251
  var form = getFormElement(meta.formId);
251
252
  if (!form) {
@@ -290,8 +291,8 @@ exports.createDummySelect = createDummySelect;
290
291
  exports.getEventTarget = getEventTarget;
291
292
  exports.getFieldElements = getFieldElements;
292
293
  exports.getFormElement = getFormElement;
294
+ exports.getInputValue = getInputValue;
293
295
  exports.isDummySelect = isDummySelect;
294
- exports.updateFieldValue = updateFieldValue;
295
296
  exports.useControl = useControl;
296
297
  exports.useInputControl = useInputControl;
297
298
  exports.useInputEvent = useInputEvent;
package/integrations.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { unstable_updateFieldValue } from '@conform-to/dom';
1
2
  import { useRef, useEffect, useMemo, useState } from 'react';
2
3
 
3
4
  function getFormElement(formId) {
@@ -50,58 +51,20 @@ function createDummySelect(form, name, value) {
50
51
  function isDummySelect(element) {
51
52
  return element.dataset.conform === 'true';
52
53
  }
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
- }
54
+ function getInputValue(element) {
55
+ if (element instanceof HTMLSelectElement) {
56
+ var _value$;
57
+ var _value = Array.from(element.selectedOptions).map(option => option.value);
58
+ return element.multiple ? _value : (_value$ = _value[0]) !== null && _value$ !== void 0 ? _value$ : null;
101
59
  }
60
+ if (element instanceof HTMLInputElement && (element.type === 'radio' || element.type === 'checkbox')) {
61
+ return element.checked ? element.value : null;
62
+ }
63
+ return element.value;
102
64
  }
103
- function useInputEvent() {
65
+ function useInputEvent(onUpdate) {
104
66
  var ref = useRef(null);
67
+ var observerRef = useRef(null);
105
68
  var eventDispatched = useRef({
106
69
  change: false,
107
70
  focus: false,
@@ -135,7 +98,9 @@ function useInputEvent() {
135
98
  eventDispatched.current.change = true;
136
99
  var element = ref.current;
137
100
  if (element) {
138
- updateFieldValue(element, value);
101
+ unstable_updateFieldValue(element, {
102
+ value
103
+ });
139
104
 
140
105
  // Dispatch input event with the updated input value
141
106
  element.dispatchEvent(new InputEvent('input', {
@@ -177,9 +142,39 @@ function useInputEvent() {
177
142
  },
178
143
  register(element) {
179
144
  ref.current = element;
145
+ if (observerRef.current) {
146
+ observerRef.current.disconnect();
147
+ observerRef.current = null;
148
+ }
149
+ if (!element) {
150
+ return;
151
+ }
152
+ observerRef.current = new MutationObserver(mutations => {
153
+ var _loop = function _loop() {
154
+ if (mutation.type === 'attributes') {
155
+ var _getInputValue;
156
+ var nextValue = (_getInputValue = getInputValue(element)) !== null && _getInputValue !== void 0 ? _getInputValue : undefined;
157
+ onUpdate(prevValue => {
158
+ if (nextValue === prevValue ||
159
+ // If the value is an array, check if the current value is the same as the new value
160
+ JSON.stringify(prevValue) === JSON.stringify(nextValue)) {
161
+ return prevValue;
162
+ }
163
+ return nextValue;
164
+ });
165
+ }
166
+ };
167
+ for (var mutation of mutations) {
168
+ _loop();
169
+ }
170
+ });
171
+ observerRef.current.observe(element, {
172
+ attributes: true,
173
+ attributeFilter: ['data-conform']
174
+ });
180
175
  }
181
176
  };
182
- }, []);
177
+ }, [onUpdate]);
183
178
  }
184
179
  function useInputValue(options) {
185
180
  var initializeValue = () => {
@@ -207,22 +202,26 @@ function useControl(meta) {
207
202
  change,
208
203
  focus,
209
204
  blur
210
- } = useInputEvent();
205
+ } = useInputEvent(
206
+ // @ts-expect-error We will fix the type when stabilizing the API
207
+ setValue);
211
208
  var handleChange = value => {
212
209
  setValue(value);
213
210
  change(value);
214
211
  };
215
212
  var refCallback = element => {
216
- var _meta$key;
217
213
  register(element);
218
214
  if (!element) {
219
215
  return;
220
216
  }
221
- var prevKey = element.dataset.conform;
222
- var nextKey = "".concat((_meta$key = meta.key) !== null && _meta$key !== void 0 ? _meta$key : '');
223
- if (prevKey !== nextKey) {
224
- element.dataset.conform = nextKey;
225
- updateFieldValue(element, value !== null && value !== void 0 ? value : '');
217
+
218
+ // We were trying to sync the value based on key previously
219
+ // This is now handled mostly by the side effect
220
+ // But we still need to set the initial value for backward compatibility
221
+ if (!element.dataset.conform) {
222
+ unstable_updateFieldValue(element, {
223
+ value
224
+ });
226
225
  }
227
226
  };
228
227
  return {
@@ -241,7 +240,9 @@ function useInputControl(meta) {
241
240
  change,
242
241
  focus,
243
242
  blur
244
- } = useInputEvent();
243
+ } = useInputEvent(
244
+ // @ts-expect-error We will fix the type when stabilizing the API
245
+ setValue);
245
246
  useEffect(() => {
246
247
  var form = getFormElement(meta.formId);
247
248
  if (!form) {
@@ -281,4 +282,4 @@ function Control(props) {
281
282
  return props.render(control);
282
283
  }
283
284
 
284
- export { Control, createDummySelect, getEventTarget, getFieldElements, getFormElement, isDummySelect, updateFieldValue, useControl, useInputControl, useInputEvent, useInputValue };
285
+ export { Control, createDummySelect, getEventTarget, getFieldElements, getFormElement, getInputValue, isDummySelect, 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.3.0",
6
+ "version": "1.5.0",
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.3.0"
33
+ "@conform-to/dom": "1.5.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@babel/core": "^7.17.8",