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