@conform-to/react 0.1.1 → 0.3.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,374 +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/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
31
+
48
32
 
49
- var handleChange = event => {
50
- var _event$target;
33
+ var handleInput = event => {
34
+ var field = event.target;
35
+ var form = ref.current;
51
36
 
52
- if (!dom.isFieldElement(event.target) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.form) !== ref.current) {
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
61
47
 
62
- if (ref.current) {
63
- dom.reportValidity(ref.current);
48
+
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
+ }
64
55
  }
65
56
  };
66
57
 
67
58
  var handleBlur = event => {
68
- var _event$target2;
59
+ var field = event.target;
60
+ var form = ref.current;
69
61
 
70
- if (!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') {
71
63
  return;
72
64
  }
73
65
 
74
- if (initialReport === 'onBlur') {
75
- dom.setFieldState(event.target, {
76
- touched: true
77
- });
78
- }
66
+ field.dataset.conformTouched = 'true';
67
+ field.reportValidity();
68
+ };
69
+
70
+ var handleReset = event => {
71
+ var form = ref.current;
79
72
 
80
- if (ref.current) {
81
- dom.reportValidity(ref.current);
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
+ }
82
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
+
88
+
89
+ setTimeout(() => {
90
+ validate === null || validate === void 0 ? void 0 : validate(form);
91
+ }, 0);
83
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
+
84
100
 
85
- document.body.addEventListener('input', handleChange);
86
- document.body.addEventListener('focusout', handleBlur);
101
+ document.addEventListener('input', handleInput, true);
102
+ document.addEventListener('blur', handleBlur, true);
103
+ document.addEventListener('reset', handleReset);
87
104
  return () => {
88
- document.body.removeEventListener('input', handleChange);
89
- document.body.removeEventListener('focusout', handleBlur);
105
+ document.removeEventListener('input', handleInput, true);
106
+ document.removeEventListener('blur', handleBlur, true);
107
+ document.removeEventListener('reset', handleReset);
90
108
  };
91
- }, [noValidate, initialReport]);
109
+ }, [validate, config.initialReport, config.noValidate]);
92
110
  return {
93
111
  ref,
94
- onSubmit: handleSubmit,
95
- onReset: handleReset,
96
- noValidate: formNoValidate
97
- };
98
- }
99
- function useFieldset(schema) {
100
- var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
101
- var ref = react.useRef(null);
102
- var [errorMessage, dispatch] = react.useReducer((state, action) => {
103
- switch (action.type) {
104
- case 'report':
105
- {
106
- var {
107
- key,
108
- message
109
- } = action.payload;
110
-
111
- if (state[key] === message) {
112
- 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';
113
132
  }
133
+ } // Check the validity of the form
114
134
 
115
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), {}, {
116
- [key]: message
117
- });
135
+
136
+ if (!event.currentTarget.reportValidity()) {
137
+ event.preventDefault();
118
138
  }
139
+ }
119
140
 
120
- case 'migrate':
121
- {
122
- var {
123
- keys,
124
- error
125
- } = action.payload;
126
- var nextState = state;
127
-
128
- for (var _key of Object.keys(keys)) {
129
- var prevError = state[_key];
130
- var nextError = error === null || error === void 0 ? void 0 : error[_key];
131
-
132
- if (typeof nextError === 'string' && prevError !== nextError) {
133
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, nextState), {}, {
134
- [_key]: nextError
135
- });
136
- }
137
- }
141
+ if (!event.defaultPrevented) {
142
+ var _config$onSubmit;
138
143
 
139
- return nextState;
140
- }
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
+ */
141
153
 
142
- case 'cleanup':
143
- {
144
- var {
145
- fieldset
146
- } = action.payload;
147
- var updates = [];
154
+ function useFieldset(ref, config) {
155
+ var [error, setError] = react.useState(() => {
156
+ var result = {};
148
157
 
149
- for (var [_key2, _message] of Object.entries(state)) {
150
- if (!_message) {
151
- continue;
152
- }
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;
153
160
 
154
- var fields = dom.getFieldElements(fieldset, _key2);
161
+ if (_error !== null && _error !== void 0 && _error.message) {
162
+ result[key] = _error.message;
163
+ }
164
+ }
155
165
 
156
- if (fields.every(field => field.validity.valid)) {
157
- 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
+ }
158
199
  }
159
200
  }
201
+ }
160
202
 
161
- if (updates.length === 0) {
162
- return state;
163
- }
203
+ return next;
204
+ });
205
+ };
164
206
 
165
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), Object.fromEntries(updates));
166
- }
207
+ var handleInput = event => {
208
+ var form = dom.getFormElement(ref.current);
209
+ var field = event.target;
167
210
 
168
- case 'reset':
169
- {
170
- return {};
171
- }
172
- }
173
- }, {}, () => Object.fromEntries(Object.keys(schema.fields).reduce((result, name) => {
174
- var _config$error;
211
+ if (!form || !dom.isFieldElement(field) || field.form !== form) {
212
+ return;
213
+ }
175
214
 
176
- var error = (_config$error = config.error) === null || _config$error === void 0 ? void 0 : _config$error[name];
215
+ resetError(form);
216
+ };
177
217
 
178
- if (typeof error === 'string') {
179
- result.push([name, error]);
180
- }
218
+ var invalidHandler = event => {
219
+ var form = dom.getFormElement(ref.current);
220
+ var field = event.target;
181
221
 
182
- return result;
183
- }, [])));
184
- react.useEffect(() => {
185
- var _schema$validate;
222
+ if (!form || !dom.isFieldElement(field) || field.form !== form) {
223
+ return;
224
+ }
186
225
 
187
- 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
188
227
 
189
- if (!fieldset) {
190
- console.warn('No fieldset ref found; You must pass the fieldsetProps to the fieldset element');
191
- return;
192
- }
228
+ if (key) {
229
+ setError(prev => {
230
+ var _prev$key;
193
231
 
194
- if (!(fieldset !== null && fieldset !== void 0 && fieldset.form)) {
195
- console.warn('No form element is linked to the fieldset; Do you forgot setting the form attribute?');
196
- }
232
+ var prevMessage = (_prev$key = prev === null || prev === void 0 ? void 0 : prev[key]) !== null && _prev$key !== void 0 ? _prev$key : '';
197
233
 
198
- (_schema$validate = schema.validate) === null || _schema$validate === void 0 ? void 0 : _schema$validate.call(schema, fieldset);
199
- dispatch({
200
- type: 'cleanup',
201
- payload: {
202
- fieldset
203
- }
204
- });
205
- }, // eslint-disable-next-line react-hooks/exhaustive-deps
206
- [schema.validate]);
207
- react.useEffect(() => {
208
- dispatch({
209
- type: 'migrate',
210
- payload: {
211
- keys: Object.keys(schema.fields),
212
- error: config.error
234
+ if (prevMessage === field.validationMessage) {
235
+ return prev;
236
+ }
237
+
238
+ return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, {
239
+ [key]: field.validationMessage
240
+ });
241
+ });
242
+ event.preventDefault();
213
243
  }
214
- });
215
- }, [config.error, schema.fields]);
216
- return [{
217
- ref,
218
- name: config.name,
219
- form: config.form,
220
-
221
- onInput(e) {
222
- var _schema$validate2;
223
-
224
- var fieldset = e.currentTarget;
225
- (_schema$validate2 = schema.validate) === null || _schema$validate2 === void 0 ? void 0 : _schema$validate2.call(schema, fieldset);
226
- dispatch({
227
- type: 'cleanup',
228
- payload: {
229
- fieldset
230
- }
231
- });
232
- },
244
+ };
245
+
246
+ var submitHandler = event => {
247
+ var form = dom.getFormElement(ref.current);
248
+
249
+ if (!form || event.target !== form) {
250
+ return;
251
+ } // This helps resetting error that fullfilled by the submitter
233
252
 
234
- onReset(e) {
235
- dom.setFieldState(e.currentTarget, {
236
- touched: false
237
- });
238
- dispatch({
239
- type: 'reset'
240
- });
241
- },
242
253
 
243
- onInvalid(e) {
244
- var element = dom.isFieldElement(e.target) ? e.target : null;
245
- var key = Object.keys(schema.fields).find(key => (element === null || element === void 0 ? void 0 : element.name) === dom.getName([e.currentTarget.name, key]));
254
+ resetError(form);
255
+ };
246
256
 
247
- if (!element || !key) {
257
+ var resetHandler = event => {
258
+ var form = dom.getFormElement(ref.current);
259
+
260
+ if (!form || event.target !== form) {
248
261
  return;
249
- } // Disable browser report
262
+ }
250
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);
271
+ document.addEventListener('reset', resetHandler);
272
+ return () => {
273
+ document.removeEventListener('input', handleInput);
274
+ document.removeEventListener('invalid', invalidHandler, true);
275
+ document.removeEventListener('submit', submitHandler);
276
+ document.removeEventListener('reset', resetHandler);
277
+ };
278
+ }, [ref, config === null || config === void 0 ? void 0 : config.name]);
279
+ react.useEffect(() => {
280
+ setError(prev => {
281
+ var next = prev;
282
+
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;
285
+
286
+ if (next[key] !== (_error2 === null || _error2 === void 0 ? void 0 : _error2.message)) {
287
+ var _error2$message;
251
288
 
252
- e.preventDefault();
253
- dispatch({
254
- type: 'report',
255
- payload: {
256
- key,
257
- 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
+ });
258
292
  }
259
- });
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;
260
323
  }
261
324
 
262
- }, dom.createFieldConfig(schema, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
263
- error: Object.assign({}, config.error, errorMessage)
264
- }))];
325
+ });
265
326
  }
266
- function useFieldList(config) {
267
- var _config$initialValue$, _config$initialValue;
268
-
269
- var size = (_config$initialValue$ = (_config$initialValue = config.initialValue) === null || _config$initialValue === void 0 ? void 0 : _config$initialValue.length) !== null && _config$initialValue$ !== void 0 ? _config$initialValue$ : 1;
270
- var [keys, setKeys] = react.useState(() => [...Array(size).keys()]);
271
- var list = react.useMemo(() => keys.map((key, index) => {
272
- var _config$initialValue2, _config$error2;
273
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/packages/conform-react/README.md#usefieldlist
333
+ */
334
+ function useFieldList(ref, config) {
335
+ var [entries, setEntries] = react.useState(() => {
336
+ var _config$defaultValue2;
337
+
338
+ return Object.entries((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [undefined]);
339
+ });
340
+ var list = entries.map((_ref, index) => {
341
+ var _config$defaultValue3, _config$initialError5, _config$initialError6;
342
+
343
+ var [key, defaultValue] = _ref;
274
344
  return {
275
- key: "".concat(key),
345
+ key,
276
346
  config: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
277
347
  name: "".concat(config.name, "[").concat(index, "]"),
278
- initialValue: (_config$initialValue2 = config.initialValue) === null || _config$initialValue2 === void 0 ? void 0 : _config$initialValue2[index],
279
- error: (_config$error2 = config.error) === null || _config$error2 === void 0 ? void 0 : _config$error2[index],
280
- constraint: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config.constraint), {}, {
281
- multiple: false
282
- })
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
283
350
  })
284
351
  };
285
- }), [keys, config]);
286
- var controls = {
287
- prepend() {
288
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.createControlButton(config.name, 'prepend', {})), {}, {
289
- onClick(e) {
290
- setKeys(keys => [Date.now(), ...keys]);
291
- e.preventDefault();
292
- }
352
+ });
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
+ }
293
373
 
294
- });
295
- },
374
+ });
375
+ react.useEffect(() => {
376
+ setEntries(prevEntries => {
377
+ var _config$defaultValue4;
296
378
 
297
- append() {
298
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.createControlButton(config.name, 'append', {})), {}, {
299
- onClick(e) {
300
- setKeys(keys => [...keys, Date.now()]);
301
- e.preventDefault();
302
- }
379
+ var nextEntries = Object.entries((_config$defaultValue4 = config.defaultValue) !== null && _config$defaultValue4 !== void 0 ? _config$defaultValue4 : [undefined]);
303
380
 
304
- });
305
- },
306
-
307
- remove(index) {
308
- return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.createControlButton(config.name, 'remove', {
309
- index
310
- })), {}, {
311
- onClick(e) {
312
- setKeys(keys => [...keys.slice(0, index), ...keys.slice(index + 1)]);
313
- e.preventDefault();
381
+ if (prevEntries.length !== nextEntries.length) {
382
+ return nextEntries;
383
+ }
384
+
385
+ for (var i = 0; i < prevEntries.length; i++) {
386
+ var [prevKey, prevValue] = prevEntries[i];
387
+ var [nextKey, nextValue] = nextEntries[i];
388
+
389
+ if (prevKey !== nextKey || prevValue !== nextValue) {
390
+ return nextEntries;
314
391
  }
392
+ } // No need to rerender in this case
315
393
 
316
- });
317
- }
318
394
 
319
- };
320
- react.useEffect(() => {
321
- setKeys(keys => {
322
- if (keys.length === size) {
323
- return keys;
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;
324
403
  }
325
404
 
326
- return [...Array(size).keys()];
327
- });
328
- }, [size]);
329
- return [list, controls];
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
+ }
432
+
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];
330
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/packages/conform-react/README.md#usecontrolledinput
452
+ */
331
453
  function useControlledInput(field) {
332
- var _field$initialValue;
454
+ var _field$defaultValue;
333
455
 
334
- var [value, setValue] = react.useState("".concat((_field$initialValue = field.initialValue) !== null && _field$initialValue !== void 0 ? _field$initialValue : ''));
335
- var [shouldBlur, setShouldBlur] = react.useState(false);
336
456
  var ref = react.useRef(null);
337
- var input = react.useMemo(() => /*#__PURE__*/react.createElement('input', _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, field.constraint), {}, {
338
- ref,
339
- name: field.name,
340
- defaultValue: field.initialValue,
341
- style: {
342
- display: 'none'
343
- },
344
- 'aria-hidden': true
345
- })), [field.constraint, field.name, field.initialValue]);
346
- react.useEffect(() => {
457
+ var [value, setValue] = react.useState("".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''));
458
+
459
+ var handleChange = eventOrValue => {
347
460
  if (!ref.current) {
348
461
  return;
349
462
  }
350
463
 
351
- ref.current.value = value;
464
+ var newValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
465
+ ref.current.value = newValue;
352
466
  ref.current.dispatchEvent(new InputEvent('input', {
353
467
  bubbles: true
354
468
  }));
355
- }, [value]);
356
- react.useEffect(() => {
357
- var _ref$current;
469
+ setValue(newValue);
470
+ };
358
471
 
359
- if (!shouldBlur) {
360
- return;
361
- }
472
+ var handleBlur = () => {
473
+ var _ref$current;
362
474
 
363
- (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.dispatchEvent(new FocusEvent('focusout', {
475
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.dispatchEvent(new FocusEvent('blur', {
364
476
  bubbles: true
365
477
  }));
366
- setShouldBlur(false);
367
- }, [shouldBlur]);
368
- return [input, {
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
+ })), {
369
490
  value,
370
- onChange: value => {
371
- setValue(value);
372
- },
373
- onBlur: () => {
374
- setShouldBlur(true);
375
- }
491
+ onChange: handleChange,
492
+ onBlur: handleBlur,
493
+ onInvalid: handleInvalid
376
494
  }];
377
495
  }
378
496