@conform-to/react 0.5.0-pre.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/hooks.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
- import { getSubmissionType, reportSubmission, getFormData, parse, isFieldElement, hasError, getPaths, getName, requestValidate, getFormElement, parseListCommand, updateList } from '@conform-to/dom';
3
- import { useRef, useState, useEffect } from 'react';
2
+ import { shouldValidate, reportSubmission, getFormData, parse, isFieldElement, hasError, getPaths, getName, requestCommand, validate, getFormElement, parseListCommand, updateList } from '@conform-to/dom';
3
+ import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react';
4
4
  import { input } from './helpers.js';
5
5
 
6
6
  /**
7
7
  * Returns properties required to hook into form events.
8
8
  * Applied custom validation and define when error should be reported.
9
9
  *
10
- * @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#useform
10
+ * @see https://conform.guide/api/react#useform
11
11
  */
12
12
  function useForm() {
13
13
  var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -21,17 +21,26 @@ function useForm() {
21
21
  })) !== null && _config$state$error$f !== void 0 ? _config$state$error$f : [];
22
22
  return message !== null && message !== void 0 ? message : '';
23
23
  });
24
- var [fieldsetConfig, setFieldsetConfig] = useState(() => {
25
- var _config$state$error2, _config$state2, _config$state$value, _config$state3;
26
- var error = (_config$state$error2 = (_config$state2 = config.state) === null || _config$state2 === void 0 ? void 0 : _config$state2.error) !== null && _config$state$error2 !== void 0 ? _config$state$error2 : [];
24
+ var [uncontrolledState, setUncontrolledState] = useState(() => {
25
+ var submission = config.state;
26
+ if (!submission) {
27
+ return {
28
+ defaultValue: config.defaultValue
29
+ };
30
+ }
27
31
  return {
28
- defaultValue: (_config$state$value = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue,
29
- initialError: error.filter(_ref2 => {
32
+ defaultValue: submission.value,
33
+ initialError: submission.error.filter(_ref2 => {
30
34
  var [name] = _ref2;
31
- return name !== '' && getSubmissionType(name) === null;
35
+ return name !== '' && shouldValidate(submission, name);
32
36
  })
33
37
  };
34
38
  });
39
+ var fieldsetConfig = _objectSpread2(_objectSpread2({}, uncontrolledState), {}, {
40
+ constraint: config.constraint,
41
+ form: config.id
42
+ });
43
+ var fieldset = useFieldset(ref, fieldsetConfig);
35
44
  var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative);
36
45
  useEffect(() => {
37
46
  configRef.current = config;
@@ -55,11 +64,8 @@ function useForm() {
55
64
  if (!form || !isFieldElement(field) || field.form !== form) {
56
65
  return;
57
66
  }
58
- if (formConfig.initialReport === 'onChange') {
59
- field.dataset.conformTouched = 'true';
60
- }
61
- if (field.dataset.conformTouched) {
62
- requestValidate(form, field.name);
67
+ if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') {
68
+ requestCommand(form, validate(field.name));
63
69
  }
64
70
  };
65
71
  var handleBlur = event => {
@@ -70,8 +76,7 @@ function useForm() {
70
76
  return;
71
77
  }
72
78
  if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) {
73
- field.dataset.conformTouched = 'true';
74
- requestValidate(form, field.name);
79
+ requestCommand(form, validate(field.name));
75
80
  }
76
81
  };
77
82
  var handleInvalid = event => {
@@ -96,11 +101,12 @@ function useForm() {
96
101
  for (var field of form.elements) {
97
102
  if (isFieldElement(field)) {
98
103
  delete field.dataset.conformTouched;
104
+ field.setAttribute('aria-invalid', 'false');
99
105
  field.setCustomValidity('');
100
106
  }
101
107
  }
102
108
  setError('');
103
- setFieldsetConfig({
109
+ setUncontrolledState({
104
110
  defaultValue: formConfig.defaultValue,
105
111
  initialError: []
106
112
  });
@@ -123,11 +129,13 @@ function useForm() {
123
129
  document.removeEventListener('reset', handleReset);
124
130
  };
125
131
  }, []);
126
- return {
132
+ var form = {
133
+ id: config.id,
127
134
  ref,
128
135
  error,
129
136
  props: {
130
137
  ref,
138
+ id: config.id,
131
139
  noValidate,
132
140
  onSubmit(event) {
133
141
  var form = event.currentTarget;
@@ -169,16 +177,6 @@ function useForm() {
169
177
  }
170
178
  }
171
179
  }
172
-
173
- // Touch all fields only if the submitter is not a command button
174
- if (submission.type === 'submit') {
175
- for (var field of form.elements) {
176
- if (isFieldElement(field)) {
177
- // Mark the field as touched
178
- field.dataset.conformTouched = 'true';
179
- }
180
- }
181
- }
182
180
  if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && hasError(submission.error) || submission.type === 'validate' && config.mode !== 'server-validation') {
183
181
  event.preventDefault();
184
182
  } else {
@@ -198,6 +196,7 @@ function useForm() {
198
196
  },
199
197
  config: fieldsetConfig
200
198
  };
199
+ return [form, fieldset];
201
200
  }
202
201
 
203
202
  /**
@@ -258,6 +257,10 @@ function useFieldset(ref, config) {
258
257
  // Update the error only if the field belongs to the fieldset
259
258
  if (typeof key === 'string' && paths.length === 0) {
260
259
  if (field.dataset.conformTouched) {
260
+ // Update the aria attribute only if it is set
261
+ if (field.getAttribute('aria-invalid')) {
262
+ field.setAttribute('aria-invalid', field.validationMessage !== '' ? 'true' : 'false');
263
+ }
261
264
  setError(prev => {
262
265
  var _prev$key;
263
266
  var prevMessage = (_prev$key = prev === null || prev === void 0 ? void 0 : prev[key]) !== null && _prev$key !== void 0 ? _prev$key : '';
@@ -312,21 +315,26 @@ function useFieldset(ref, config) {
312
315
  var field = {
313
316
  config: _objectSpread2({
314
317
  name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key,
315
- form: fieldsetConfig.form,
316
318
  defaultValue: uncontrolledState.defaultValue[key],
317
319
  initialError: uncontrolledState.initialError[key]
318
320
  }, constraint),
319
321
  error: (_error$key = error === null || error === void 0 ? void 0 : error[key]) !== null && _error$key !== void 0 ? _error$key : ''
320
322
  };
323
+ if (fieldsetConfig.form) {
324
+ field.config.form = fieldsetConfig.form;
325
+ field.config.id = "".concat(fieldsetConfig.form, "-").concat(field.config.name);
326
+ field.config.errorId = "".concat(field.config.id, "-error");
327
+ }
321
328
  return field;
322
329
  }
323
330
  });
324
331
  }
332
+
325
333
  /**
326
334
  * Returns a list of key and config, with a group of helpers
327
335
  * configuring buttons for list manipulation
328
336
  *
329
- * @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usefieldlist
337
+ * @see https://conform.guide/api/react#usefieldlist
330
338
  */
331
339
  function useFieldList(ref, config) {
332
340
  var configRef = useRef(config);
@@ -357,41 +365,6 @@ function useFieldList(ref, config) {
357
365
  var _config$defaultValue3;
358
366
  return Object.entries((_config$defaultValue3 = config.defaultValue) !== null && _config$defaultValue3 !== void 0 ? _config$defaultValue3 : [undefined]);
359
367
  });
360
- var list = entries.map((_ref3, index) => {
361
- var [key, defaultValue] = _ref3;
362
- return {
363
- key,
364
- error: error[index],
365
- config: {
366
- name: "".concat(config.name, "[").concat(index, "]"),
367
- form: config.form,
368
- defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index],
369
- initialError: uncontrolledState.initialError[index]
370
- }
371
- };
372
- });
373
-
374
- /***
375
- * This use proxy to capture all information about the command and
376
- * have it encoded in the value.
377
- */
378
- var command = new Proxy({}, {
379
- get(_target, type) {
380
- return function () {
381
- var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
382
- return {
383
- name: 'conform/list',
384
- value: JSON.stringify({
385
- type,
386
- scope: config.name,
387
- payload
388
- }),
389
- form: config.form,
390
- formNoValidate: true
391
- };
392
- };
393
- }
394
- });
395
368
  useEffect(() => {
396
369
  configRef.current = config;
397
370
  });
@@ -488,16 +461,32 @@ function useFieldList(ref, config) {
488
461
  document.removeEventListener('reset', resetHandler);
489
462
  };
490
463
  }, [ref]);
491
- return [list,
492
- // @ts-expect-error proxy type
493
- command];
464
+ return entries.map((_ref3, index) => {
465
+ var [key, defaultValue] = _ref3;
466
+ var fieldConfig = {
467
+ name: "".concat(config.name, "[").concat(index, "]"),
468
+ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index],
469
+ initialError: uncontrolledState.initialError[index]
470
+ };
471
+ if (config.form) {
472
+ fieldConfig.form = config.form;
473
+ fieldConfig.id = "".concat(config.form, "-").concat(config.name);
474
+ fieldConfig.errorId = "".concat(fieldConfig.id, "-error");
475
+ }
476
+ return {
477
+ key,
478
+ error: error[index],
479
+ config: fieldConfig
480
+ };
481
+ });
494
482
  }
495
483
  /**
496
484
  * Returns the properties required to configure a shadow input for validation.
497
485
  * This is particular useful when integrating dropdown and datepicker whichs
498
486
  * introduces custom input mode.
499
487
  *
500
- * @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usecontrolledinput
488
+ * @deprecated Please use the `useInputEvent` hook instead
489
+ * @see https://conform.guide/api/react#usecontrolledinput
501
490
  */
502
491
  function useControlledInput(config) {
503
492
  var _config$defaultValue4;
@@ -552,24 +541,13 @@ function useControlledInput(config) {
552
541
  }, []);
553
542
  return [_objectSpread2({
554
543
  ref,
555
- style: {
556
- position: 'absolute',
557
- width: '1px',
558
- height: '1px',
559
- padding: 0,
560
- margin: '-1px',
561
- overflow: 'hidden',
562
- clip: 'rect(0,0,0,0)',
563
- whiteSpace: 'nowrap',
564
- borderWidth: 0
565
- },
566
- tabIndex: -1,
567
- 'aria-hidden': true,
568
544
  onFocus() {
569
545
  var _inputRef$current;
570
546
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
571
547
  }
572
- }, input(_objectSpread2(_objectSpread2({}, config), uncontrolledState))), {
548
+ }, input(_objectSpread2(_objectSpread2({}, config), uncontrolledState), {
549
+ hidden: true
550
+ })), {
573
551
  ref: inputRef,
574
552
  value,
575
553
  onChange: handleChange,
@@ -578,4 +556,179 @@ function useControlledInput(config) {
578
556
  }];
579
557
  }
580
558
 
581
- export { useControlledInput, useFieldList, useFieldset, useForm };
559
+ /**
560
+ * Triggering react custom change event
561
+ * Solution based on dom-testing-library
562
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
563
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
564
+ */
565
+ function setNativeValue(element, value) {
566
+ if (element.value === value) {
567
+ // It will not trigger a change event if `element.value` is the same as the set value
568
+ return;
569
+ }
570
+ var {
571
+ set: valueSetter
572
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
573
+ var prototype = Object.getPrototypeOf(element);
574
+ var {
575
+ set: prototypeValueSetter
576
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
577
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
578
+ prototypeValueSetter.call(element, value);
579
+ } else {
580
+ if (valueSetter) {
581
+ valueSetter.call(element, value);
582
+ } else {
583
+ throw new Error('The given element does not have a value setter');
584
+ }
585
+ }
586
+ }
587
+
588
+ /**
589
+ * useLayoutEffect is client-only.
590
+ * This basically makes it a no-op on server
591
+ */
592
+ var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
593
+ function useInputEvent(options) {
594
+ var ref = useRef(null);
595
+ var optionsRef = useRef(options);
596
+ var changeDispatched = useRef(false);
597
+ var focusDispatched = useRef(false);
598
+ var blurDispatched = useRef(false);
599
+ useSafeLayoutEffect(() => {
600
+ optionsRef.current = options;
601
+ });
602
+ useSafeLayoutEffect(() => {
603
+ var getInputElement = () => {
604
+ var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2;
605
+ return (_optionsRef$current$g = (_optionsRef$current = optionsRef.current) === null || _optionsRef$current === void 0 ? void 0 : (_optionsRef$current$g2 = _optionsRef$current.getElement) === null || _optionsRef$current$g2 === void 0 ? void 0 : _optionsRef$current$g2.call(_optionsRef$current, ref.current)) !== null && _optionsRef$current$g !== void 0 ? _optionsRef$current$g : ref.current;
606
+ };
607
+ var inputHandler = event => {
608
+ var input = getInputElement();
609
+ if (input && event.target === input) {
610
+ changeDispatched.current = true;
611
+ }
612
+ };
613
+ var focusHandler = event => {
614
+ var input = getInputElement();
615
+ if (input && event.target === input) {
616
+ focusDispatched.current = true;
617
+ }
618
+ };
619
+ var blurHandler = event => {
620
+ var input = getInputElement();
621
+ if (input && event.target === input) {
622
+ blurDispatched.current = true;
623
+ }
624
+ };
625
+ var submitHandler = event => {
626
+ var input = getInputElement();
627
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
628
+ var _optionsRef$current2, _optionsRef$current2$;
629
+ (_optionsRef$current2 = optionsRef.current) === null || _optionsRef$current2 === void 0 ? void 0 : (_optionsRef$current2$ = _optionsRef$current2.onSubmit) === null || _optionsRef$current2$ === void 0 ? void 0 : _optionsRef$current2$.call(_optionsRef$current2, event);
630
+ }
631
+ };
632
+ var resetHandler = event => {
633
+ var input = getInputElement();
634
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
635
+ var _optionsRef$current3, _optionsRef$current3$;
636
+ (_optionsRef$current3 = optionsRef.current) === null || _optionsRef$current3 === void 0 ? void 0 : (_optionsRef$current3$ = _optionsRef$current3.onReset) === null || _optionsRef$current3$ === void 0 ? void 0 : _optionsRef$current3$.call(_optionsRef$current3, event);
637
+ }
638
+ };
639
+ document.addEventListener('input', inputHandler, true);
640
+ document.addEventListener('focus', focusHandler, true);
641
+ document.addEventListener('blur', blurHandler, true);
642
+ document.addEventListener('submit', submitHandler);
643
+ document.addEventListener('reset', resetHandler);
644
+ return () => {
645
+ document.removeEventListener('input', inputHandler, true);
646
+ document.removeEventListener('focus', focusHandler, true);
647
+ document.removeEventListener('blur', blurHandler, true);
648
+ document.removeEventListener('submit', submitHandler);
649
+ document.removeEventListener('reset', resetHandler);
650
+ };
651
+ }, []);
652
+ var control = useMemo(() => {
653
+ var getInputElement = () => {
654
+ var _optionsRef$current$g3, _optionsRef$current4, _optionsRef$current4$;
655
+ return (_optionsRef$current$g3 = (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 ? void 0 : (_optionsRef$current4$ = _optionsRef$current4.getElement) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, ref.current)) !== null && _optionsRef$current$g3 !== void 0 ? _optionsRef$current$g3 : ref.current;
656
+ };
657
+ return {
658
+ change(eventOrValue) {
659
+ var input = getInputElement();
660
+ if (!input) {
661
+ console.warn('Missing input ref; No change-related events will be dispatched');
662
+ return;
663
+ }
664
+ if (changeDispatched.current) {
665
+ changeDispatched.current = false;
666
+ return;
667
+ }
668
+ var previousValue = input.value;
669
+ var nextValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
670
+
671
+ // This make sure no event is dispatched on the first effect run
672
+ if (nextValue === previousValue) {
673
+ return;
674
+ }
675
+
676
+ // Dispatch beforeinput event before updating the input value
677
+ input.dispatchEvent(new Event('beforeinput', {
678
+ bubbles: true
679
+ }));
680
+ // Update the input value to trigger a change event
681
+ setNativeValue(input, nextValue);
682
+ // Dispatch input event with the updated input value
683
+ input.dispatchEvent(new InputEvent('input', {
684
+ bubbles: true
685
+ }));
686
+ // Reset the dispatched flag
687
+ changeDispatched.current = false;
688
+ },
689
+ focus() {
690
+ var input = getInputElement();
691
+ if (!input) {
692
+ console.warn('Missing input ref; No focus-related events will be dispatched');
693
+ return;
694
+ }
695
+ if (focusDispatched.current) {
696
+ focusDispatched.current = false;
697
+ return;
698
+ }
699
+ var focusinEvent = new FocusEvent('focusin', {
700
+ bubbles: true
701
+ });
702
+ var focusEvent = new FocusEvent('focus');
703
+ input.dispatchEvent(focusinEvent);
704
+ input.dispatchEvent(focusEvent);
705
+
706
+ // Reset the dispatched flag
707
+ focusDispatched.current = false;
708
+ },
709
+ blur() {
710
+ var input = getInputElement();
711
+ if (!input) {
712
+ console.warn('Missing input ref; No blur-related events will be dispatched');
713
+ return;
714
+ }
715
+ if (blurDispatched.current) {
716
+ blurDispatched.current = false;
717
+ return;
718
+ }
719
+ var focusoutEvent = new FocusEvent('focusout', {
720
+ bubbles: true
721
+ });
722
+ var blurEvent = new FocusEvent('blur');
723
+ input.dispatchEvent(focusoutEvent);
724
+ input.dispatchEvent(blurEvent);
725
+
726
+ // Reset the dispatched flag
727
+ blurDispatched.current = false;
728
+ }
729
+ };
730
+ }, []);
731
+ return [ref, control];
732
+ }
733
+
734
+ export { useControlledInput, useFieldList, useFieldset, useForm, useInputEvent };
package/module/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { getFormElements, hasError, parse, shouldValidate } from '@conform-to/dom';
2
- export { useControlledInput, useFieldList, useFieldset, useForm } from './hooks.js';
1
+ export { getFormElements, hasError, list, parse, requestCommand, requestSubmit, shouldValidate, validate } from '@conform-to/dom';
2
+ export { useControlledInput, useFieldList, useFieldset, useForm, useInputEvent } from './hooks.js';
3
3
  import * as helpers from './helpers.js';
4
4
  export { helpers as conform };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@conform-to/react",
3
3
  "description": "Conform view adapter for react",
4
4
  "license": "MIT",
5
- "version": "0.5.0-pre.0",
5
+ "version": "0.5.1",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/edmundhung/conform/issues"
20
20
  },
21
21
  "dependencies": {
22
- "@conform-to/dom": "0.5.0-pre.0"
22
+ "@conform-to/dom": "0.5.1"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "react": ">=16.8"