@conform-to/react 0.2.0 → 0.3.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hooks.js CHANGED
@@ -5,416 +5,492 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js');
6
6
  var dom = require('@conform-to/dom');
7
7
  var react = require('react');
8
-
8
+ var helpers = require('./helpers.js');
9
+
10
+ /**
11
+ * Returns properties required to hook into form events.
12
+ * Applied custom validation and define when error should be reported.
13
+ *
14
+ * @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#useform
15
+ */
9
16
  function useForm() {
17
+ var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
10
18
  var {
11
- onReset,
12
- onSubmit,
13
- noValidate = false,
14
- fallbackNative = false,
15
- initialReport = 'onSubmit'
16
- } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
19
+ validate
20
+ } = config;
17
21
  var ref = react.useRef(null);
18
- var [formNoValidate, setFormNoValidate] = react.useState(noValidate || !fallbackNative);
19
-
20
- var handleSubmit = event => {
21
- if (!noValidate) {
22
- dom.setFieldState(event.currentTarget, {
23
- touched: true
24
- });
25
-
26
- if (!dom.shouldSkipValidate(event.nativeEvent) && !event.currentTarget.reportValidity()) {
27
- return event.preventDefault();
28
- }
29
- }
30
-
31
- onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(event);
32
- };
33
-
34
- var handleReset = event => {
35
- dom.setFieldState(event.currentTarget, {
36
- touched: false
37
- });
38
- onReset === null || onReset === void 0 ? void 0 : onReset(event);
39
- };
40
-
22
+ var [noValidate, setNoValidate] = react.useState(config.noValidate || !config.fallbackNative);
41
23
  react.useEffect(() => {
42
- setFormNoValidate(true);
24
+ setNoValidate(true);
43
25
  }, []);
44
26
  react.useEffect(() => {
45
- if (noValidate) {
46
- return;
47
- }
27
+ // Initialize form validation messages
28
+ if (ref.current) {
29
+ validate === null || validate === void 0 ? void 0 : validate(ref.current);
30
+ } // Revalidate the form when input value is changed
48
31
 
49
- var handleChange = event => {
50
- var _event$target;
51
32
 
52
- if (!ref.current || !dom.isFieldElement(event.target) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.form) !== ref.current) {
33
+ var handleInput = event => {
34
+ var field = event.target;
35
+ var form = ref.current;
36
+
37
+ if (!form || !dom.isFieldElement(field) || field.form !== form) {
53
38
  return;
54
39
  }
55
40
 
56
- if (initialReport === 'onChange') {
57
- dom.setFieldState(event.target, {
58
- touched: true
59
- });
60
- }
41
+ validate === null || validate === void 0 ? void 0 : validate(form);
42
+
43
+ if (!config.noValidate) {
44
+ if (config.initialReport === 'onChange') {
45
+ field.dataset.conformTouched = 'true';
46
+ } // Field validity might be changed due to cross reference
47
+
61
48
 
62
- dom.reportValidity(ref.current);
49
+ for (var _field of form.elements) {
50
+ if (dom.isFieldElement(_field) && _field.dataset.conformTouched) {
51
+ // Report latest error for all touched fields
52
+ _field.checkValidity();
53
+ }
54
+ }
55
+ }
63
56
  };
64
57
 
65
58
  var handleBlur = event => {
66
- var _event$target2;
59
+ var field = event.target;
60
+ var form = ref.current;
67
61
 
68
- if (!ref.current || !dom.isFieldElement(event.target) || ((_event$target2 = event.target) === null || _event$target2 === void 0 ? void 0 : _event$target2.form) !== ref.current) {
62
+ if (!form || !dom.isFieldElement(field) || field.form !== form || config.noValidate || config.initialReport !== 'onBlur') {
69
63
  return;
70
64
  }
71
65
 
72
- if (initialReport === 'onBlur') {
73
- dom.setFieldState(event.target, {
74
- touched: true
75
- });
66
+ field.dataset.conformTouched = 'true';
67
+ field.reportValidity();
68
+ };
69
+
70
+ var handleReset = event => {
71
+ var form = ref.current;
72
+
73
+ if (!form || event.target !== form) {
74
+ return;
75
+ } // Reset all field state
76
+
77
+
78
+ for (var field of form.elements) {
79
+ if (dom.isFieldElement(field)) {
80
+ delete field.dataset.conformTouched;
81
+ }
76
82
  }
83
+ /**
84
+ * The reset event is triggered before form reset happens.
85
+ * This make sure the form to be revalidated with initial values.
86
+ */
87
+
77
88
 
78
- dom.reportValidity(ref.current);
89
+ setTimeout(() => {
90
+ validate === null || validate === void 0 ? void 0 : validate(form);
91
+ }, 0);
79
92
  };
93
+ /**
94
+ * The input event handler will be triggered in capturing phase in order to
95
+ * allow follow-up action in the bubble phase based on the latest validity
96
+ * E.g. `useFieldset` reset the error of valid field after checking the
97
+ * validity in the bubble phase.
98
+ */
99
+
80
100
 
81
- document.addEventListener('input', handleChange);
82
- document.addEventListener('focusout', handleBlur);
101
+ document.addEventListener('input', handleInput, true);
102
+ document.addEventListener('blur', handleBlur, true);
103
+ document.addEventListener('reset', handleReset);
83
104
  return () => {
84
- document.removeEventListener('input', handleChange);
85
- document.removeEventListener('focusout', handleBlur);
105
+ document.removeEventListener('input', handleInput, true);
106
+ document.removeEventListener('blur', handleBlur, true);
107
+ document.removeEventListener('reset', handleReset);
86
108
  };
87
- }, [noValidate, initialReport]);
109
+ }, [validate, config.initialReport, config.noValidate]);
88
110
  return {
89
111
  ref,
90
- onSubmit: handleSubmit,
91
- onReset: handleReset,
92
- noValidate: formNoValidate
93
- };
94
- }
95
- function useFieldset(schema) {
96
- var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
97
- var ref = react.useRef(null);
98
- var [errorMessage, dispatch] = react.useReducer((state, action) => {
99
- switch (action.type) {
100
- case 'report':
101
- {
102
- var {
103
- key,
104
- message
105
- } = action.payload;
106
-
107
- if (state[key] === message) {
108
- return state;
112
+ noValidate,
113
+
114
+ onSubmit(event) {
115
+ var form = event.currentTarget;
116
+ var nativeEvent = event.nativeEvent;
117
+ var submitter = nativeEvent.submitter instanceof HTMLButtonElement || nativeEvent.submitter instanceof HTMLInputElement ? nativeEvent.submitter : null; // Validating the form with the submitter value
118
+
119
+ validate === null || validate === void 0 ? void 0 : validate(form, submitter);
120
+ /**
121
+ * It checks defaultPrevented to confirm if the submission is intentional
122
+ * This is utilized by `useFieldList` to modify the list state when the submit
123
+ * event is captured and revalidate the form with new fields without triggering
124
+ * a form submission at the same time.
125
+ */
126
+
127
+ if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && !event.defaultPrevented) {
128
+ // Mark all fields as touched
129
+ for (var field of form.elements) {
130
+ if (dom.isFieldElement(field)) {
131
+ field.dataset.conformTouched = 'true';
109
132
  }
133
+ } // Check the validity of the form
110
134
 
111
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), {}, {
112
- [key]: message
113
- });
135
+
136
+ if (!event.currentTarget.reportValidity()) {
137
+ event.preventDefault();
114
138
  }
139
+ }
115
140
 
116
- case 'migrate':
117
- {
118
- var {
119
- keys,
120
- error
121
- } = action.payload;
122
- var nextState = state;
123
-
124
- for (var _key of Object.keys(keys)) {
125
- var prevError = state[_key];
126
- var nextError = error === null || error === void 0 ? void 0 : error[_key];
127
-
128
- if (typeof nextError === 'string' && prevError !== nextError) {
129
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, nextState), {}, {
130
- [_key]: nextError
131
- });
132
- }
133
- }
141
+ if (!event.defaultPrevented) {
142
+ var _config$onSubmit;
134
143
 
135
- return nextState;
136
- }
144
+ (_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event);
145
+ }
146
+ }
147
+
148
+ };
149
+ }
150
+ /**
151
+ * All the information of the field, including state and config.
152
+ */
137
153
 
138
- case 'cleanup':
139
- {
140
- var {
141
- fieldset
142
- } = action.payload;
143
- var updates = [];
154
+ function useFieldset(ref, config) {
155
+ var [error, setError] = react.useState(() => {
156
+ var result = {};
144
157
 
145
- for (var [_key2, _message] of Object.entries(state)) {
146
- if (!_message) {
147
- continue;
148
- }
158
+ for (var [key, _error] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
159
+ var _config$initialError;
149
160
 
150
- var fields = dom.getFieldElements(fieldset, _key2);
161
+ if (_error !== null && _error !== void 0 && _error.message) {
162
+ result[key] = _error.message;
163
+ }
164
+ }
151
165
 
152
- if (fields.every(field => field.validity.valid)) {
153
- updates.push([_key2, '']);
166
+ return result;
167
+ });
168
+ react.useEffect(() => {
169
+ /**
170
+ * Reset the error state of each field if its validity is changed.
171
+ *
172
+ * This is a workaround as no official way is provided to notify
173
+ * when the validity of the field is changed from `invalid` to `valid`.
174
+ */
175
+ var resetError = form => {
176
+ setError(prev => {
177
+ var next = prev;
178
+
179
+ for (var field of form.elements) {
180
+ if (dom.isFieldElement(field)) {
181
+ var key = dom.getKey(field.name, config === null || config === void 0 ? void 0 : config.name);
182
+
183
+ if (key) {
184
+ var _next$key, _next;
185
+
186
+ var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
187
+ var nextMessage = field.validationMessage;
188
+ /**
189
+ * Techincally, checking prevMessage not being empty while nextMessage being empty
190
+ * is sufficient for our usecase. It checks if the message is changed instead to allow
191
+ * the hook to be useful independently.
192
+ */
193
+
194
+ if (prevMessage !== '' && prevMessage !== nextMessage) {
195
+ next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
196
+ [key]: nextMessage
197
+ });
198
+ }
154
199
  }
155
200
  }
201
+ }
156
202
 
157
- if (updates.length === 0) {
158
- return state;
159
- }
203
+ return next;
204
+ });
205
+ };
160
206
 
161
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), Object.fromEntries(updates));
162
- }
207
+ var handleInput = event => {
208
+ var form = dom.getFormElement(ref.current);
209
+ var field = event.target;
163
210
 
164
- case 'reset':
165
- {
166
- return {};
167
- }
168
- }
169
- }, {}, () => Object.fromEntries(Object.keys(schema.fields).reduce((result, name) => {
170
- var _config$error;
211
+ if (!form || !dom.isFieldElement(field) || field.form !== form) {
212
+ return;
213
+ }
171
214
 
172
- var error = (_config$error = config.error) === null || _config$error === void 0 ? void 0 : _config$error[name];
215
+ resetError(form);
216
+ };
173
217
 
174
- if (typeof error === 'string') {
175
- result.push([name, error]);
176
- }
218
+ var invalidHandler = event => {
219
+ var form = dom.getFormElement(ref.current);
220
+ var field = event.target;
177
221
 
178
- return result;
179
- }, [])));
180
- react.useEffect(() => {
181
- var _schema$validate;
222
+ if (!form || !dom.isFieldElement(field) || field.form !== form) {
223
+ return;
224
+ }
182
225
 
183
- var fieldset = ref.current;
226
+ var key = dom.getKey(field.name, config === null || config === void 0 ? void 0 : config.name); // Update the error only if the field belongs to the fieldset
184
227
 
185
- if (!fieldset) {
186
- console.warn('No fieldset ref found; You must pass the fieldsetProps to the fieldset element');
187
- return;
188
- }
228
+ if (key) {
229
+ setError(prev => {
230
+ var _prev$key;
189
231
 
190
- if (!(fieldset !== null && fieldset !== void 0 && fieldset.form)) {
191
- console.warn('No form element is linked to the fieldset; Do you forgot setting the form attribute?');
192
- }
232
+ var prevMessage = (_prev$key = prev === null || prev === void 0 ? void 0 : prev[key]) !== null && _prev$key !== void 0 ? _prev$key : '';
233
+
234
+ if (prevMessage === field.validationMessage) {
235
+ return prev;
236
+ }
193
237
 
194
- (_schema$validate = schema.validate) === null || _schema$validate === void 0 ? void 0 : _schema$validate.call(schema, fieldset);
195
- dispatch({
196
- type: 'cleanup',
197
- payload: {
198
- fieldset
238
+ return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, {
239
+ [key]: field.validationMessage
240
+ });
241
+ });
242
+ event.preventDefault();
199
243
  }
200
- });
244
+ };
245
+
246
+ var submitHandler = event => {
247
+ var form = dom.getFormElement(ref.current);
201
248
 
202
- var resetHandler = e => {
203
- if (e.target !== fieldset.form) {
249
+ if (!form || event.target !== form) {
204
250
  return;
205
- }
251
+ } // This helps resetting error that fullfilled by the submitter
206
252
 
207
- dispatch({
208
- type: 'reset'
209
- });
210
- setTimeout(() => {
211
- var _schema$validate2;
212
253
 
213
- // Delay revalidation until reset is completed
214
- (_schema$validate2 = schema.validate) === null || _schema$validate2 === void 0 ? void 0 : _schema$validate2.call(schema, fieldset);
215
- }, 0);
254
+ resetError(form);
216
255
  };
217
256
 
257
+ var resetHandler = event => {
258
+ var form = dom.getFormElement(ref.current);
259
+
260
+ if (!form || event.target !== form) {
261
+ return;
262
+ }
263
+
264
+ setError({});
265
+ };
266
+
267
+ document.addEventListener('input', handleInput); // The invalid event does not bubble and so listening on the capturing pharse is needed
268
+
269
+ document.addEventListener('invalid', invalidHandler, true);
270
+ document.addEventListener('submit', submitHandler);
218
271
  document.addEventListener('reset', resetHandler);
219
272
  return () => {
273
+ document.removeEventListener('input', handleInput);
274
+ document.removeEventListener('invalid', invalidHandler, true);
275
+ document.removeEventListener('submit', submitHandler);
220
276
  document.removeEventListener('reset', resetHandler);
221
277
  };
222
- }, // eslint-disable-next-line react-hooks/exhaustive-deps
223
- [schema.validate]);
278
+ }, [ref, config === null || config === void 0 ? void 0 : config.name]);
224
279
  react.useEffect(() => {
225
- dispatch({
226
- type: 'migrate',
227
- payload: {
228
- keys: Object.keys(schema.fields),
229
- error: config.error
230
- }
231
- });
232
- }, [config.error, schema.fields]);
233
- return [{
234
- ref,
235
- name: config.name,
236
- form: config.form,
237
-
238
- onInput(e) {
239
- var _schema$validate3;
240
-
241
- var fieldset = e.currentTarget;
242
- (_schema$validate3 = schema.validate) === null || _schema$validate3 === void 0 ? void 0 : _schema$validate3.call(schema, fieldset);
243
- dispatch({
244
- type: 'cleanup',
245
- payload: {
246
- fieldset
247
- }
248
- });
249
- },
280
+ setError(prev => {
281
+ var next = prev;
250
282
 
251
- onInvalid(e) {
252
- var element = dom.isFieldElement(e.target) ? e.target : null;
253
- var key = Object.keys(schema.fields).find(key => (element === null || element === void 0 ? void 0 : element.name) === dom.getName([e.currentTarget.name, key]));
254
-
255
- if (!element || !key) {
256
- return;
257
- } // Disable browser report
283
+ for (var [key, _error2] of Object.entries((_config$initialError2 = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {})) {
284
+ var _config$initialError2;
258
285
 
286
+ if (next[key] !== (_error2 === null || _error2 === void 0 ? void 0 : _error2.message)) {
287
+ var _error2$message;
259
288
 
260
- e.preventDefault();
261
- dispatch({
262
- type: 'report',
263
- payload: {
264
- key,
265
- message: element.validationMessage
289
+ next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
290
+ [key]: (_error2$message = _error2 === null || _error2 === void 0 ? void 0 : _error2.message) !== null && _error2$message !== void 0 ? _error2$message : ''
291
+ });
266
292
  }
267
- });
293
+ }
294
+
295
+ return next;
296
+ });
297
+ }, [config === null || config === void 0 ? void 0 : config.name, config === null || config === void 0 ? void 0 : config.initialError]);
298
+ /**
299
+ * This allows us constructing the field at runtime as we have no information
300
+ * about which fields would be available. The proxy will also help tracking
301
+ * the usage of each field for optimization in the future.
302
+ */
303
+
304
+ return new Proxy({}, {
305
+ get(_target, key) {
306
+ var _constraint, _config$defaultValue, _config$initialError3, _config$initialError4, _error$key;
307
+
308
+ if (typeof key !== 'string') {
309
+ return;
310
+ }
311
+
312
+ var constraint = config === null || config === void 0 ? void 0 : (_constraint = config.constraint) === null || _constraint === void 0 ? void 0 : _constraint[key];
313
+ var field = {
314
+ config: _rollupPluginBabelHelpers.objectSpread2({
315
+ name: config !== null && config !== void 0 && config.name ? "".concat(config.name, ".").concat(key) : key,
316
+ form: config === null || config === void 0 ? void 0 : config.form,
317
+ defaultValue: config === null || config === void 0 ? void 0 : (_config$defaultValue = config.defaultValue) === null || _config$defaultValue === void 0 ? void 0 : _config$defaultValue[key],
318
+ initialError: config === null || config === void 0 ? void 0 : (_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : (_config$initialError4 = _config$initialError3[key]) === null || _config$initialError4 === void 0 ? void 0 : _config$initialError4.details
319
+ }, constraint),
320
+ error: (_error$key = error === null || error === void 0 ? void 0 : error[key]) !== null && _error$key !== void 0 ? _error$key : ''
321
+ };
322
+ return field;
268
323
  }
269
324
 
270
- }, dom.getFieldProps(schema, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
271
- error: Object.assign({}, config.error, errorMessage)
272
- }))];
325
+ });
273
326
  }
274
- function useFieldList(props) {
327
+
328
+ /**
329
+ * Returns a list of key and config, with a group of helpers
330
+ * configuring buttons for list manipulation
331
+ *
332
+ * @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usefieldlist
333
+ */
334
+ function useFieldList(ref, config) {
275
335
  var [entries, setEntries] = react.useState(() => {
276
- var _props$defaultValue;
336
+ var _config$defaultValue2;
277
337
 
278
- return Object.entries((_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : [undefined]);
338
+ return Object.entries((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [undefined]);
279
339
  });
280
340
  var list = entries.map((_ref, index) => {
281
- var _props$defaultValue2, _props$error;
341
+ var _config$defaultValue3, _config$initialError5, _config$initialError6;
282
342
 
283
343
  var [key, defaultValue] = _ref;
284
344
  return {
285
- key: "".concat(key),
286
- props: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, props), {}, {
287
- name: "".concat(props.name, "[").concat(index, "]"),
288
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_props$defaultValue2 = props.defaultValue) === null || _props$defaultValue2 === void 0 ? void 0 : _props$defaultValue2[index],
289
- error: (_props$error = props.error) === null || _props$error === void 0 ? void 0 : _props$error[index],
290
- multiple: false
345
+ key,
346
+ config: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
347
+ name: "".concat(config.name, "[").concat(index, "]"),
348
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue3 = config.defaultValue) === null || _config$defaultValue3 === void 0 ? void 0 : _config$defaultValue3[index],
349
+ initialError: (_config$initialError5 = config.initialError) === null || _config$initialError5 === void 0 ? void 0 : (_config$initialError6 = _config$initialError5[index]) === null || _config$initialError6 === void 0 ? void 0 : _config$initialError6.details
291
350
  })
292
351
  };
293
352
  });
294
- var controls = {
295
- prepend(defaultValue) {
296
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'prepend', {
297
- defaultValue
298
- })), {}, {
299
- onClick(e) {
300
- setEntries(entries => dom.applyControlCommand([...entries], 'prepend', {
301
- defaultValue: ["".concat(Date.now()), defaultValue]
302
- }));
303
- e.preventDefault();
304
- }
353
+ /***
354
+ * This use proxy to capture all information about the command and
355
+ * have it encoded in the value.
356
+ */
357
+
358
+ var control = new Proxy({}, {
359
+ get(_target, type) {
360
+ return function () {
361
+ var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
362
+ return {
363
+ name: dom.listCommandKey,
364
+ value: dom.serializeListCommand(config.name, {
365
+ type,
366
+ payload
367
+ }),
368
+ form: config.form,
369
+ formNoValidate: true
370
+ };
371
+ };
372
+ }
305
373
 
306
- });
307
- },
308
-
309
- append(defaultValue) {
310
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'append', {
311
- defaultValue
312
- })), {}, {
313
- onClick(e) {
314
- setEntries(entries => dom.applyControlCommand([...entries], 'append', {
315
- defaultValue: ["".concat(Date.now()), defaultValue]
316
- }));
317
- e.preventDefault();
318
- }
374
+ });
375
+ react.useEffect(() => {
376
+ setEntries(prevEntries => {
377
+ var _config$defaultValue4;
319
378
 
320
- });
321
- },
322
-
323
- replace(index, defaultValue) {
324
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'replace', {
325
- index,
326
- defaultValue
327
- })), {}, {
328
- onClick(e) {
329
- setEntries(entries => dom.applyControlCommand([...entries], 'replace', {
330
- defaultValue: ["".concat(Date.now()), defaultValue],
331
- index
332
- }));
333
- e.preventDefault();
334
- }
379
+ var nextEntries = Object.entries((_config$defaultValue4 = config.defaultValue) !== null && _config$defaultValue4 !== void 0 ? _config$defaultValue4 : [undefined]);
335
380
 
336
- });
337
- },
338
-
339
- remove(index) {
340
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'remove', {
341
- index
342
- })), {}, {
343
- onClick(e) {
344
- setEntries(entries => dom.applyControlCommand([...entries], 'remove', {
345
- index
346
- }));
347
- e.preventDefault();
348
- }
381
+ if (prevEntries.length !== nextEntries.length) {
382
+ return nextEntries;
383
+ }
349
384
 
350
- });
351
- },
352
-
353
- reorder(fromIndex, toIndex) {
354
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'reorder', {
355
- from: fromIndex,
356
- to: toIndex
357
- })), {}, {
358
- onClick(e) {
359
- if (fromIndex !== toIndex) {
360
- setEntries(entries => dom.applyControlCommand([...entries], 'reorder', {
361
- from: fromIndex,
362
- to: toIndex
363
- }));
364
- }
385
+ for (var i = 0; i < prevEntries.length; i++) {
386
+ var [prevKey, prevValue] = prevEntries[i];
387
+ var [nextKey, nextValue] = nextEntries[i];
365
388
 
366
- e.preventDefault();
389
+ if (prevKey !== nextKey || prevValue !== nextValue) {
390
+ return nextEntries;
367
391
  }
392
+ } // No need to rerender in this case
368
393
 
369
- });
370
- }
371
394
 
372
- };
373
- react.useEffect(() => {
374
- var _props$defaultValue3;
395
+ return prevEntries;
396
+ });
397
+
398
+ var submitHandler = event => {
399
+ var form = dom.getFormElement(ref.current);
400
+
401
+ if (!form || event.target !== form || !(event.submitter instanceof HTMLButtonElement) || event.submitter.name !== dom.listCommandKey) {
402
+ return;
403
+ }
404
+
405
+ var [name, command] = dom.parseListCommand(event.submitter.value);
406
+
407
+ if (name !== config.name) {
408
+ // Ensure the scope of the listener are limited to specific field name
409
+ return;
410
+ }
411
+
412
+ switch (command.type) {
413
+ case 'append':
414
+ case 'prepend':
415
+ case 'replace':
416
+ command.payload.defaultValue = ["".concat(Date.now()), command.payload.defaultValue];
417
+ break;
418
+ }
419
+
420
+ setEntries(entries => dom.updateList([...(entries !== null && entries !== void 0 ? entries : [])], command));
421
+ event.preventDefault();
422
+ };
423
+
424
+ var resetHandler = event => {
425
+ var _config$defaultValue5;
426
+
427
+ var form = dom.getFormElement(ref.current);
428
+
429
+ if (!form || event.target !== form) {
430
+ return;
431
+ }
375
432
 
376
- setEntries(Object.entries((_props$defaultValue3 = props.defaultValue) !== null && _props$defaultValue3 !== void 0 ? _props$defaultValue3 : [undefined]));
377
- }, [props.defaultValue]);
378
- return [list, controls];
433
+ setEntries(Object.entries((_config$defaultValue5 = config.defaultValue) !== null && _config$defaultValue5 !== void 0 ? _config$defaultValue5 : []));
434
+ };
435
+
436
+ document.addEventListener('submit', submitHandler, true);
437
+ document.addEventListener('reset', resetHandler);
438
+ return () => {
439
+ document.removeEventListener('submit', submitHandler, true);
440
+ document.removeEventListener('reset', resetHandler);
441
+ };
442
+ }, [ref, config.name, config.defaultValue]);
443
+ return [list, control];
379
444
  }
445
+
446
+ /**
447
+ * Returns the properties required to configure a shadow input for validation.
448
+ * This is particular useful when integrating dropdown and datepicker whichs
449
+ * introduces custom input mode.
450
+ *
451
+ * @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usecontrolledinput
452
+ */
380
453
  function useControlledInput(field) {
381
- var _ref$current$value, _ref$current, _field$defaultValue;
454
+ var _field$defaultValue;
382
455
 
383
456
  var ref = react.useRef(null);
384
- var input = react.useMemo(() => /*#__PURE__*/react.createElement('input', {
385
- ref,
386
- name: field.name,
387
- form: field.form,
388
- defaultValue: field.defaultValue,
389
- required: field.required,
390
- minLength: field.minLength,
391
- maxLength: field.maxLength,
392
- min: field.min,
393
- max: field.max,
394
- step: field.step,
395
- pattern: field.pattern,
396
- hidden: true,
397
- 'aria-hidden': true
398
- }), [field.name, field.form, field.defaultValue, field.required, field.minLength, field.maxLength, field.min, field.max, field.step, field.pattern]);
399
- return [input, {
400
- value: (_ref$current$value = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.value) !== null && _ref$current$value !== void 0 ? _ref$current$value : "".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''),
401
- onChange: value => {
402
- if (!ref.current) {
403
- return;
404
- }
457
+ var [value, setValue] = react.useState("".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''));
405
458
 
406
- ref.current.value = value;
407
- ref.current.dispatchEvent(new InputEvent('input', {
408
- bubbles: true
409
- }));
410
- },
411
- onBlur: () => {
412
- var _ref$current2;
413
-
414
- (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.dispatchEvent(new FocusEvent('focusout', {
415
- bubbles: true
416
- }));
459
+ var handleChange = eventOrValue => {
460
+ if (!ref.current) {
461
+ return;
417
462
  }
463
+
464
+ var newValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
465
+ ref.current.value = newValue;
466
+ ref.current.dispatchEvent(new InputEvent('input', {
467
+ bubbles: true
468
+ }));
469
+ setValue(newValue);
470
+ };
471
+
472
+ var handleBlur = () => {
473
+ var _ref$current;
474
+
475
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.dispatchEvent(new FocusEvent('blur', {
476
+ bubbles: true
477
+ }));
478
+ };
479
+
480
+ var handleInvalid = event => {
481
+ event.preventDefault();
482
+ };
483
+
484
+ return [_rollupPluginBabelHelpers.objectSpread2({
485
+ ref,
486
+ hidden: true
487
+ }, helpers.input(field, {
488
+ type: 'text'
489
+ })), {
490
+ value,
491
+ onChange: handleChange,
492
+ onBlur: handleBlur,
493
+ onInvalid: handleInvalid
418
494
  }];
419
495
  }
420
496