@conform-to/dom 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ ```
2
+ ███████╗ ██████╗ ███╗ ██╗ ████████╗ ██████╗ ███████╗ ███╗ ███╗
3
+ ██╔═════╝ ██╔═══██╗ ████╗ ██║ ██╔═════╝ ██╔═══██╗ ██╔═══██╗ ████████║
4
+ ██║ ██║ ██║ ██╔██╗██║ ███████╗ ██║ ██║ ███████╔╝ ██╔██╔██║
5
+ ██║ ██║ ██║ ██║╚████║ ██╔════╝ ██║ ██║ ██╔═══██╗ ██║╚═╝██║
6
+ ╚███████╗ ╚██████╔╝ ██║ ╚███║ ██║ ╚██████╔╝ ██║ ██║ ██║ ██║
7
+ ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
+ ```
9
+
10
+ Version 1.5.0 / License MIT / Copyright (c) 2024 Edmund Hung
11
+
12
+ A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
13
+
14
+ # Getting Started
15
+
16
+ Check out the overview and tutorial at our website https://conform.guide
17
+
18
+ # Features
19
+
20
+ - Progressive enhancement first APIs
21
+ - Type-safe field inference
22
+ - Fine-grained subscription
23
+ - Built-in accessibility helpers
24
+ - Automatic type coercion with Zod
25
+
26
+ # Documentation
27
+
28
+ - Validation: https://conform.guide/validation
29
+ - Nested object and Array: https://conform.guide/complex-structures
30
+ - UI Integrations: https://conform.guide/integration/ui-libraries
31
+ - Intent button: https://conform.guide/intent-button
32
+ - Accessibility Guide: https://conform.guide/accessibility
33
+
34
+ # Support
35
+
36
+ To report a bug, please open an issue on the repository at https://github.com/edmundhung/conform. For feature requests and questions, you can post them in the Discussions section.
package/form.d.ts CHANGED
@@ -35,6 +35,7 @@ export type Constraint = {
35
35
  export type FormMeta<FormError> = {
36
36
  formId: string;
37
37
  isValueUpdated: boolean;
38
+ pendingIntents: Intent[];
38
39
  submissionStatus?: 'error' | 'success';
39
40
  defaultValue: Record<string, unknown>;
40
41
  initialValue: Record<string, unknown>;
@@ -98,6 +99,7 @@ export type SubscriptionSubject = {
98
99
  } & {
99
100
  formId?: boolean;
100
101
  status?: boolean;
102
+ pendingIntents?: boolean;
101
103
  };
102
104
  export type SubscriptionScope = {
103
105
  prefix?: string[];
@@ -123,6 +125,7 @@ export type FormContext<Schema extends Record<string, any> = any, FormError = st
123
125
  onBlur(event: Event): void;
124
126
  onUpdate(options: Partial<FormOptions<Schema, FormError, FormValue>>): void;
125
127
  observe(): () => void;
128
+ runSideEffect(intents: Intent[]): void;
126
129
  subscribe(callback: () => void, getSubject?: () => SubscriptionSubject | undefined): () => void;
127
130
  getState(): FormState<FormError>;
128
131
  getSerializedState(): string;
@@ -144,4 +147,14 @@ export type FormContext<Schema extends Record<string, any> = any, FormError = st
144
147
  };
145
148
  };
146
149
  export declare function createFormContext<Schema extends Record<string, any>, FormError = string[], FormValue = Schema>(options: FormOptions<Schema, FormError, FormValue>): FormContext<Schema, FormError, FormValue>;
150
+ /**
151
+ * Updates the DOM element with the provided value.
152
+ *
153
+ * @param element The form element to update
154
+ * @param options The options to update the form element
155
+ */
156
+ export declare function updateFieldValue(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
157
+ value?: string | string[];
158
+ defaultValue?: string | string[];
159
+ }): void;
147
160
  export {};
package/form.js CHANGED
@@ -8,13 +8,17 @@ var dom = require('./dom.js');
8
8
  var util = require('./util.js');
9
9
  var submission = require('./submission.js');
10
10
 
11
- function createFormMeta(options, initialized) {
11
+ function createFormMeta(options, isResetting) {
12
12
  var _lastResult$initialVa, _options$constraint, _lastResult$state$val, _lastResult$state, _ref;
13
- var lastResult = !initialized ? options.lastResult : undefined;
13
+ var lastResult = !isResetting ? options.lastResult : undefined;
14
14
  var defaultValue = options.defaultValue ? submission.serialize(options.defaultValue) : {};
15
15
  var initialValue = (_lastResult$initialVa = lastResult === null || lastResult === void 0 ? void 0 : lastResult.initialValue) !== null && _lastResult$initialVa !== void 0 ? _lastResult$initialVa : defaultValue;
16
16
  var result = {
17
17
  formId: options.formId,
18
+ pendingIntents: isResetting ? [{
19
+ type: 'reset',
20
+ payload: {}
21
+ }] : [],
18
22
  isValueUpdated: false,
19
23
  submissionStatus: lastResult === null || lastResult === void 0 ? void 0 : lastResult.status,
20
24
  defaultValue,
@@ -22,7 +26,7 @@ function createFormMeta(options, initialized) {
22
26
  value: initialValue,
23
27
  constraint: (_options$constraint = options.constraint) !== null && _options$constraint !== void 0 ? _options$constraint : {},
24
28
  validated: (_lastResult$state$val = lastResult === null || lastResult === void 0 || (_lastResult$state = lastResult.state) === null || _lastResult$state === void 0 ? void 0 : _lastResult$state.validated) !== null && _lastResult$state$val !== void 0 ? _lastResult$state$val : {},
25
- key: !initialized ? getDefaultKey(defaultValue) : _rollupPluginBabelHelpers.objectSpread2({
29
+ key: !isResetting ? getDefaultKey(defaultValue) : _rollupPluginBabelHelpers.objectSpread2({
26
30
  '': util.generateId()
27
31
  }, getDefaultKey(defaultValue)),
28
32
  // The `lastResult` should comes from the server which we won't expect the error to be null
@@ -282,6 +286,7 @@ function shouldNotify(prev, next, cache, scope) {
282
286
  function createFormContext(options) {
283
287
  var subscribers = [];
284
288
  var latestOptions = options;
289
+ var processedIntents = new Set();
285
290
  var meta = createFormMeta(options);
286
291
  var state = createFormState(meta);
287
292
  function getFormElement() {
@@ -295,6 +300,7 @@ function createFormContext(options) {
295
300
  var value = next.value === next.initialValue ? initialValue : !state || prev.value !== next.value ? createValueProxy(next.value) : state.value;
296
301
  return {
297
302
  submissionStatus: next.submissionStatus,
303
+ pendingIntents: next.pendingIntents,
298
304
  defaultValue,
299
305
  initialValue,
300
306
  value,
@@ -328,7 +334,7 @@ function createFormContext(options) {
328
334
  for (var subscriber of subscribers) {
329
335
  var _subscriber$getSubjec;
330
336
  var subject = (_subscriber$getSubjec = subscriber.getSubject) === null || _subscriber$getSubjec === void 0 ? void 0 : _subscriber$getSubjec.call(subscriber);
331
- if (!subject || subject.formId && prevMeta.formId !== nextMeta.formId || subject.status && prevState.submissionStatus !== nextState.submissionStatus || shouldNotify(prevState.error, nextState.error, cache.error, subject.error) || shouldNotify(prevState.initialValue, nextState.initialValue, cache.initialValue, subject.initialValue) || shouldNotify(prevState.key, nextState.key, cache.key, subject.key, (prev, next) => prev !== next) || shouldNotify(prevState.valid, nextState.valid, cache.valid, subject.valid, compareBoolean) || shouldNotify(prevState.dirty, nextState.dirty, cache.dirty, subject.dirty, compareBoolean) || shouldNotify(prevState.value, nextState.value, cache.value, subject.value)) {
337
+ if (!subject || subject.formId && prevMeta.formId !== nextMeta.formId || subject.status && prevState.submissionStatus !== nextState.submissionStatus || subject.pendingIntents && prevMeta.pendingIntents !== nextMeta.pendingIntents || shouldNotify(prevState.error, nextState.error, cache.error, subject.error) || shouldNotify(prevState.initialValue, nextState.initialValue, cache.initialValue, subject.initialValue) || shouldNotify(prevState.key, nextState.key, cache.key, subject.key, (prev, next) => prev !== next) || shouldNotify(prevState.valid, nextState.valid, cache.valid, subject.valid, compareBoolean) || shouldNotify(prevState.dirty, nextState.dirty, cache.dirty, subject.dirty, compareBoolean) || shouldNotify(prevState.value, nextState.value, cache.value, subject.value)) {
332
338
  subscriber.callback();
333
339
  }
334
340
  }
@@ -425,6 +431,7 @@ function createFormContext(options) {
425
431
  });
426
432
  }
427
433
  function reset() {
434
+ processedIntents.clear();
428
435
  updateFormMeta(createFormMeta(latestOptions, true));
429
436
  }
430
437
  function onReset(event) {
@@ -449,7 +456,9 @@ function createFormContext(options) {
449
456
  }
450
457
  return result;
451
458
  }, {});
459
+ var pendingIntents = result.intent ? meta.pendingIntents.filter(intent => !processedIntents.has(intent)).concat(result.intent) : meta.pendingIntents;
452
460
  var update = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, meta), {}, {
461
+ pendingIntents,
453
462
  isValueUpdated: false,
454
463
  submissionStatus: result.status,
455
464
  value: result.initialValue,
@@ -557,6 +566,55 @@ function createFormContext(options) {
557
566
  observer.disconnect();
558
567
  };
559
568
  }
569
+ function runSideEffect(intents) {
570
+ var formElement = getFormElement();
571
+ if (!formElement) {
572
+ return;
573
+ }
574
+ for (var intent of intents) {
575
+ switch (intent.type) {
576
+ case 'update':
577
+ {
578
+ var _name5 = formdata.formatName(intent.payload.name, intent.payload.index);
579
+ var parentPaths = formdata.getPaths(_name5);
580
+ for (var element of formElement.elements) {
581
+ if (dom.isFieldElement(element)) {
582
+ var paths = formdata.getChildPaths(parentPaths, element.name);
583
+ if (paths) {
584
+ var value = formdata.getValue(intent.payload.value, formdata.formatPaths(paths));
585
+ updateFieldValue(element, {
586
+ value: typeof value === 'string' || Array.isArray(value) && value.every(item => typeof item === 'string') ? value : ''
587
+ });
588
+
589
+ // Update the element attribute to notify useControl / useInputControl hook
590
+ element.dataset.conform = util.generateId();
591
+ }
592
+ }
593
+ }
594
+ break;
595
+ }
596
+ case 'reset':
597
+ {
598
+ var prefix = formdata.formatName(intent.payload.name, intent.payload.index);
599
+ for (var _element of formElement.elements) {
600
+ if (dom.isFieldElement(_element) && formdata.isPrefix(_element.name, prefix)) {
601
+ var _value2 = formdata.getValue(meta.defaultValue, _element.name);
602
+ var defaultValue = typeof _value2 === 'string' || Array.isArray(_value2) && _value2.every(item => typeof item === 'string') ? _value2 : _element instanceof HTMLSelectElement ? [] : '';
603
+ updateFieldValue(_element, {
604
+ defaultValue,
605
+ value: defaultValue
606
+ });
607
+
608
+ // Update the element attribute to notify useControl / useInputControl hook
609
+ _element.dataset.conform = util.generateId();
610
+ }
611
+ }
612
+ break;
613
+ }
614
+ }
615
+ processedIntents.add(intent);
616
+ }
617
+ }
560
618
  return {
561
619
  getFormId() {
562
620
  return meta.formId;
@@ -572,6 +630,7 @@ function createFormContext(options) {
572
630
  insert: createFormControl('insert'),
573
631
  remove: createFormControl('remove'),
574
632
  reorder: createFormControl('reorder'),
633
+ runSideEffect,
575
634
  subscribe,
576
635
  getState,
577
636
  getSerializedState,
@@ -579,4 +638,97 @@ function createFormContext(options) {
579
638
  };
580
639
  }
581
640
 
641
+ /**
642
+ * Updates the DOM element with the provided value.
643
+ *
644
+ * @param element The form element to update
645
+ * @param options The options to update the form element
646
+ */
647
+ function updateFieldValue(element, options) {
648
+ var value = typeof options.value === 'undefined' ? null : Array.isArray(options.value) ? Array.from(options.value) : [options.value];
649
+ var defaultValue = typeof options.defaultValue === 'undefined' ? null : Array.isArray(options.defaultValue) ? Array.from(options.defaultValue) : [options.defaultValue];
650
+ if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
651
+ if (value) {
652
+ element.checked = value.includes(element.value);
653
+ }
654
+ if (defaultValue) {
655
+ element.defaultChecked = defaultValue.includes(element.value);
656
+ }
657
+ } else if (element instanceof HTMLSelectElement) {
658
+ // If the select element is not multiple and the value is an empty array, unset the selected index
659
+ // This is to prevent the select element from showing the first option as selected
660
+ if (value && value.length === 0 && !element.multiple) {
661
+ element.selectedIndex = -1;
662
+ }
663
+ for (var option of element.options) {
664
+ if (value) {
665
+ var index = value.indexOf(option.value);
666
+ var selected = index > -1;
667
+
668
+ // Update the selected state of the option
669
+ if (option.selected !== selected) {
670
+ option.selected = selected;
671
+ }
672
+
673
+ // Remove the option from the value array
674
+ if (selected) {
675
+ value.splice(index, 1);
676
+ }
677
+ }
678
+ if (defaultValue) {
679
+ var _index = defaultValue.indexOf(option.value);
680
+ var _selected = _index > -1;
681
+
682
+ // Update the selected state of the option
683
+ if (option.selected !== _selected) {
684
+ option.defaultSelected = _selected;
685
+ }
686
+
687
+ // Remove the option from the defaultValue array
688
+ if (_selected) {
689
+ defaultValue.splice(_index, 1);
690
+ }
691
+ }
692
+ }
693
+
694
+ // We have already removed all selected options from the value and defaultValue array at this point
695
+ var missingOptions = new Set([...(value !== null && value !== void 0 ? value : []), ...(defaultValue !== null && defaultValue !== void 0 ? defaultValue : [])]);
696
+ for (var optionValue of missingOptions) {
697
+ element.options.add(new Option(optionValue, optionValue, defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.includes(optionValue), value === null || value === void 0 ? void 0 : value.includes(optionValue)));
698
+ }
699
+ } else {
700
+ if (value) {
701
+ var _value$;
702
+ /**
703
+ * Triggering react custom change event
704
+ * Solution based on dom-testing-library
705
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
706
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
707
+ */
708
+ var inputValue = (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : '';
709
+ var {
710
+ set: valueSetter
711
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
712
+ var prototype = Object.getPrototypeOf(element);
713
+ var {
714
+ set: prototypeValueSetter
715
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
716
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
717
+ prototypeValueSetter.call(element, inputValue);
718
+ } else {
719
+ if (valueSetter) {
720
+ valueSetter.call(element, inputValue);
721
+ } else {
722
+ throw new Error('The given element does not have a value setter');
723
+ }
724
+ }
725
+ }
726
+ if (defaultValue) {
727
+ var _defaultValue$;
728
+ element.defaultValue = (_defaultValue$ = defaultValue[0]) !== null && _defaultValue$ !== void 0 ? _defaultValue$ : '';
729
+ }
730
+ }
731
+ }
732
+
582
733
  exports.createFormContext = createFormContext;
734
+ exports.updateFieldValue = updateFieldValue;
package/form.mjs CHANGED
@@ -1,16 +1,20 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
2
- import { flatten, formatName, getValue, isPlainObject, isPrefix, setValue, normalize, getFormData, getPaths, formatPaths } from './formdata.mjs';
2
+ import { flatten, formatName, getValue, isPlainObject, isPrefix, setValue, normalize, getFormData, getPaths, getChildPaths, formatPaths } from './formdata.mjs';
3
3
  import { getFormAction, getFormEncType, getFormMethod, isFieldElement, requestSubmit } from './dom.mjs';
4
4
  import { generateId, clone, invariant } from './util.mjs';
5
5
  import { serialize, setListState, setListValue, setState, INTENT, serializeIntent, root, getSubmissionContext } from './submission.mjs';
6
6
 
7
- function createFormMeta(options, initialized) {
7
+ function createFormMeta(options, isResetting) {
8
8
  var _lastResult$initialVa, _options$constraint, _lastResult$state$val, _lastResult$state, _ref;
9
- var lastResult = !initialized ? options.lastResult : undefined;
9
+ var lastResult = !isResetting ? options.lastResult : undefined;
10
10
  var defaultValue = options.defaultValue ? serialize(options.defaultValue) : {};
11
11
  var initialValue = (_lastResult$initialVa = lastResult === null || lastResult === void 0 ? void 0 : lastResult.initialValue) !== null && _lastResult$initialVa !== void 0 ? _lastResult$initialVa : defaultValue;
12
12
  var result = {
13
13
  formId: options.formId,
14
+ pendingIntents: isResetting ? [{
15
+ type: 'reset',
16
+ payload: {}
17
+ }] : [],
14
18
  isValueUpdated: false,
15
19
  submissionStatus: lastResult === null || lastResult === void 0 ? void 0 : lastResult.status,
16
20
  defaultValue,
@@ -18,7 +22,7 @@ function createFormMeta(options, initialized) {
18
22
  value: initialValue,
19
23
  constraint: (_options$constraint = options.constraint) !== null && _options$constraint !== void 0 ? _options$constraint : {},
20
24
  validated: (_lastResult$state$val = lastResult === null || lastResult === void 0 || (_lastResult$state = lastResult.state) === null || _lastResult$state === void 0 ? void 0 : _lastResult$state.validated) !== null && _lastResult$state$val !== void 0 ? _lastResult$state$val : {},
21
- key: !initialized ? getDefaultKey(defaultValue) : _objectSpread2({
25
+ key: !isResetting ? getDefaultKey(defaultValue) : _objectSpread2({
22
26
  '': generateId()
23
27
  }, getDefaultKey(defaultValue)),
24
28
  // The `lastResult` should comes from the server which we won't expect the error to be null
@@ -278,6 +282,7 @@ function shouldNotify(prev, next, cache, scope) {
278
282
  function createFormContext(options) {
279
283
  var subscribers = [];
280
284
  var latestOptions = options;
285
+ var processedIntents = new Set();
281
286
  var meta = createFormMeta(options);
282
287
  var state = createFormState(meta);
283
288
  function getFormElement() {
@@ -291,6 +296,7 @@ function createFormContext(options) {
291
296
  var value = next.value === next.initialValue ? initialValue : !state || prev.value !== next.value ? createValueProxy(next.value) : state.value;
292
297
  return {
293
298
  submissionStatus: next.submissionStatus,
299
+ pendingIntents: next.pendingIntents,
294
300
  defaultValue,
295
301
  initialValue,
296
302
  value,
@@ -324,7 +330,7 @@ function createFormContext(options) {
324
330
  for (var subscriber of subscribers) {
325
331
  var _subscriber$getSubjec;
326
332
  var subject = (_subscriber$getSubjec = subscriber.getSubject) === null || _subscriber$getSubjec === void 0 ? void 0 : _subscriber$getSubjec.call(subscriber);
327
- if (!subject || subject.formId && prevMeta.formId !== nextMeta.formId || subject.status && prevState.submissionStatus !== nextState.submissionStatus || shouldNotify(prevState.error, nextState.error, cache.error, subject.error) || shouldNotify(prevState.initialValue, nextState.initialValue, cache.initialValue, subject.initialValue) || shouldNotify(prevState.key, nextState.key, cache.key, subject.key, (prev, next) => prev !== next) || shouldNotify(prevState.valid, nextState.valid, cache.valid, subject.valid, compareBoolean) || shouldNotify(prevState.dirty, nextState.dirty, cache.dirty, subject.dirty, compareBoolean) || shouldNotify(prevState.value, nextState.value, cache.value, subject.value)) {
333
+ if (!subject || subject.formId && prevMeta.formId !== nextMeta.formId || subject.status && prevState.submissionStatus !== nextState.submissionStatus || subject.pendingIntents && prevMeta.pendingIntents !== nextMeta.pendingIntents || shouldNotify(prevState.error, nextState.error, cache.error, subject.error) || shouldNotify(prevState.initialValue, nextState.initialValue, cache.initialValue, subject.initialValue) || shouldNotify(prevState.key, nextState.key, cache.key, subject.key, (prev, next) => prev !== next) || shouldNotify(prevState.valid, nextState.valid, cache.valid, subject.valid, compareBoolean) || shouldNotify(prevState.dirty, nextState.dirty, cache.dirty, subject.dirty, compareBoolean) || shouldNotify(prevState.value, nextState.value, cache.value, subject.value)) {
328
334
  subscriber.callback();
329
335
  }
330
336
  }
@@ -421,6 +427,7 @@ function createFormContext(options) {
421
427
  });
422
428
  }
423
429
  function reset() {
430
+ processedIntents.clear();
424
431
  updateFormMeta(createFormMeta(latestOptions, true));
425
432
  }
426
433
  function onReset(event) {
@@ -445,7 +452,9 @@ function createFormContext(options) {
445
452
  }
446
453
  return result;
447
454
  }, {});
455
+ var pendingIntents = result.intent ? meta.pendingIntents.filter(intent => !processedIntents.has(intent)).concat(result.intent) : meta.pendingIntents;
448
456
  var update = _objectSpread2(_objectSpread2({}, meta), {}, {
457
+ pendingIntents,
449
458
  isValueUpdated: false,
450
459
  submissionStatus: result.status,
451
460
  value: result.initialValue,
@@ -553,6 +562,55 @@ function createFormContext(options) {
553
562
  observer.disconnect();
554
563
  };
555
564
  }
565
+ function runSideEffect(intents) {
566
+ var formElement = getFormElement();
567
+ if (!formElement) {
568
+ return;
569
+ }
570
+ for (var intent of intents) {
571
+ switch (intent.type) {
572
+ case 'update':
573
+ {
574
+ var _name5 = formatName(intent.payload.name, intent.payload.index);
575
+ var parentPaths = getPaths(_name5);
576
+ for (var element of formElement.elements) {
577
+ if (isFieldElement(element)) {
578
+ var paths = getChildPaths(parentPaths, element.name);
579
+ if (paths) {
580
+ var value = getValue(intent.payload.value, formatPaths(paths));
581
+ updateFieldValue(element, {
582
+ value: typeof value === 'string' || Array.isArray(value) && value.every(item => typeof item === 'string') ? value : ''
583
+ });
584
+
585
+ // Update the element attribute to notify useControl / useInputControl hook
586
+ element.dataset.conform = generateId();
587
+ }
588
+ }
589
+ }
590
+ break;
591
+ }
592
+ case 'reset':
593
+ {
594
+ var prefix = formatName(intent.payload.name, intent.payload.index);
595
+ for (var _element of formElement.elements) {
596
+ if (isFieldElement(_element) && isPrefix(_element.name, prefix)) {
597
+ var _value2 = getValue(meta.defaultValue, _element.name);
598
+ var defaultValue = typeof _value2 === 'string' || Array.isArray(_value2) && _value2.every(item => typeof item === 'string') ? _value2 : _element instanceof HTMLSelectElement ? [] : '';
599
+ updateFieldValue(_element, {
600
+ defaultValue,
601
+ value: defaultValue
602
+ });
603
+
604
+ // Update the element attribute to notify useControl / useInputControl hook
605
+ _element.dataset.conform = generateId();
606
+ }
607
+ }
608
+ break;
609
+ }
610
+ }
611
+ processedIntents.add(intent);
612
+ }
613
+ }
556
614
  return {
557
615
  getFormId() {
558
616
  return meta.formId;
@@ -568,6 +626,7 @@ function createFormContext(options) {
568
626
  insert: createFormControl('insert'),
569
627
  remove: createFormControl('remove'),
570
628
  reorder: createFormControl('reorder'),
629
+ runSideEffect,
571
630
  subscribe,
572
631
  getState,
573
632
  getSerializedState,
@@ -575,4 +634,96 @@ function createFormContext(options) {
575
634
  };
576
635
  }
577
636
 
578
- export { createFormContext };
637
+ /**
638
+ * Updates the DOM element with the provided value.
639
+ *
640
+ * @param element The form element to update
641
+ * @param options The options to update the form element
642
+ */
643
+ function updateFieldValue(element, options) {
644
+ var value = typeof options.value === 'undefined' ? null : Array.isArray(options.value) ? Array.from(options.value) : [options.value];
645
+ var defaultValue = typeof options.defaultValue === 'undefined' ? null : Array.isArray(options.defaultValue) ? Array.from(options.defaultValue) : [options.defaultValue];
646
+ if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
647
+ if (value) {
648
+ element.checked = value.includes(element.value);
649
+ }
650
+ if (defaultValue) {
651
+ element.defaultChecked = defaultValue.includes(element.value);
652
+ }
653
+ } else if (element instanceof HTMLSelectElement) {
654
+ // If the select element is not multiple and the value is an empty array, unset the selected index
655
+ // This is to prevent the select element from showing the first option as selected
656
+ if (value && value.length === 0 && !element.multiple) {
657
+ element.selectedIndex = -1;
658
+ }
659
+ for (var option of element.options) {
660
+ if (value) {
661
+ var index = value.indexOf(option.value);
662
+ var selected = index > -1;
663
+
664
+ // Update the selected state of the option
665
+ if (option.selected !== selected) {
666
+ option.selected = selected;
667
+ }
668
+
669
+ // Remove the option from the value array
670
+ if (selected) {
671
+ value.splice(index, 1);
672
+ }
673
+ }
674
+ if (defaultValue) {
675
+ var _index = defaultValue.indexOf(option.value);
676
+ var _selected = _index > -1;
677
+
678
+ // Update the selected state of the option
679
+ if (option.selected !== _selected) {
680
+ option.defaultSelected = _selected;
681
+ }
682
+
683
+ // Remove the option from the defaultValue array
684
+ if (_selected) {
685
+ defaultValue.splice(_index, 1);
686
+ }
687
+ }
688
+ }
689
+
690
+ // We have already removed all selected options from the value and defaultValue array at this point
691
+ var missingOptions = new Set([...(value !== null && value !== void 0 ? value : []), ...(defaultValue !== null && defaultValue !== void 0 ? defaultValue : [])]);
692
+ for (var optionValue of missingOptions) {
693
+ element.options.add(new Option(optionValue, optionValue, defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.includes(optionValue), value === null || value === void 0 ? void 0 : value.includes(optionValue)));
694
+ }
695
+ } else {
696
+ if (value) {
697
+ var _value$;
698
+ /**
699
+ * Triggering react custom change event
700
+ * Solution based on dom-testing-library
701
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
702
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
703
+ */
704
+ var inputValue = (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : '';
705
+ var {
706
+ set: valueSetter
707
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
708
+ var prototype = Object.getPrototypeOf(element);
709
+ var {
710
+ set: prototypeValueSetter
711
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
712
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
713
+ prototypeValueSetter.call(element, inputValue);
714
+ } else {
715
+ if (valueSetter) {
716
+ valueSetter.call(element, inputValue);
717
+ } else {
718
+ throw new Error('The given element does not have a value setter');
719
+ }
720
+ }
721
+ }
722
+ if (defaultValue) {
723
+ var _defaultValue$;
724
+ element.defaultValue = (_defaultValue$ = defaultValue[0]) !== null && _defaultValue$ !== void 0 ? _defaultValue$ : '';
725
+ }
726
+ }
727
+ }
728
+
729
+ export { createFormContext, updateFieldValue };
package/formdata.d.ts CHANGED
@@ -30,6 +30,11 @@ export declare function formatName(prefix: string | undefined, path?: string | n
30
30
  * Check if a name match the prefix paths
31
31
  */
32
32
  export declare function isPrefix(name: string, prefix: string): boolean;
33
+ /**
34
+ * Compare the parent and child paths to get the relative paths
35
+ * Returns null if the child paths do not start with the parent paths
36
+ */
37
+ export declare function getChildPaths(parentNameOrPaths: string | Array<string | number>, childName: string): (string | number)[] | null;
33
38
  /**
34
39
  * Assign a value to a target object by following the paths
35
40
  */
package/formdata.js CHANGED
@@ -81,6 +81,19 @@ function isPrefix(name, prefix) {
81
81
  return paths.length >= prefixPaths.length && prefixPaths.every((path, index) => paths[index] === path);
82
82
  }
83
83
 
84
+ /**
85
+ * Compare the parent and child paths to get the relative paths
86
+ * Returns null if the child paths do not start with the parent paths
87
+ */
88
+ function getChildPaths(parentNameOrPaths, childName) {
89
+ var parentPaths = typeof parentNameOrPaths === 'string' ? getPaths(parentNameOrPaths) : parentNameOrPaths;
90
+ var childPaths = getPaths(childName);
91
+ if (childPaths.length >= parentPaths.length && parentPaths.every((path, index) => childPaths[index] === path)) {
92
+ return childPaths.slice(parentPaths.length);
93
+ }
94
+ return null;
95
+ }
96
+
84
97
  /**
85
98
  * Assign a value to a target object by following the paths
86
99
  */
@@ -204,6 +217,7 @@ function flatten(data) {
204
217
  exports.flatten = flatten;
205
218
  exports.formatName = formatName;
206
219
  exports.formatPaths = formatPaths;
220
+ exports.getChildPaths = getChildPaths;
207
221
  exports.getFormData = getFormData;
208
222
  exports.getPaths = getPaths;
209
223
  exports.getValue = getValue;
package/formdata.mjs CHANGED
@@ -77,6 +77,19 @@ function isPrefix(name, prefix) {
77
77
  return paths.length >= prefixPaths.length && prefixPaths.every((path, index) => paths[index] === path);
78
78
  }
79
79
 
80
+ /**
81
+ * Compare the parent and child paths to get the relative paths
82
+ * Returns null if the child paths do not start with the parent paths
83
+ */
84
+ function getChildPaths(parentNameOrPaths, childName) {
85
+ var parentPaths = typeof parentNameOrPaths === 'string' ? getPaths(parentNameOrPaths) : parentNameOrPaths;
86
+ var childPaths = getPaths(childName);
87
+ if (childPaths.length >= parentPaths.length && parentPaths.every((path, index) => childPaths[index] === path)) {
88
+ return childPaths.slice(parentPaths.length);
89
+ }
90
+ return null;
91
+ }
92
+
80
93
  /**
81
94
  * Assign a value to a target object by following the paths
82
95
  */
@@ -197,4 +210,4 @@ function flatten(data) {
197
210
  return result;
198
211
  }
199
212
 
200
- export { flatten, formatName, formatPaths, getFormData, getPaths, getValue, isFile, isPlainObject, isPrefix, normalize, setValue };
213
+ export { flatten, formatName, formatPaths, getChildPaths, getFormData, getPaths, getValue, isFile, isPlainObject, isPrefix, normalize, setValue };
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type Combine, type Constraint, type ControlButtonProps, type FormId, type FieldName, type DefaultValue, type FormValue, type FormOptions, type FormState, type FormContext, type SubscriptionSubject, type SubscriptionScope, createFormContext as unstable_createFormContext, } from './form';
1
+ export { type Combine, type Constraint, type ControlButtonProps, type FormId, type FieldName, type DefaultValue, type FormValue, type FormOptions, type FormState, type FormContext, type SubscriptionSubject, type SubscriptionScope, createFormContext as unstable_createFormContext, updateFieldValue as unstable_updateFieldValue, } from './form';
2
2
  export { type FieldElement, isFieldElement } from './dom';
3
3
  export { type Submission, type SubmissionResult, type Intent, INTENT, STATE, serializeIntent, parse, } from './submission';
4
4
  export { getPaths, formatPaths, isPrefix } from './formdata';
package/index.js CHANGED
@@ -10,6 +10,7 @@ var formdata = require('./formdata.js');
10
10
 
11
11
 
12
12
  exports.unstable_createFormContext = form.createFormContext;
13
+ exports.unstable_updateFieldValue = form.updateFieldValue;
13
14
  exports.isFieldElement = dom.isFieldElement;
14
15
  exports.INTENT = submission.INTENT;
15
16
  exports.STATE = submission.STATE;
package/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { createFormContext as unstable_createFormContext } from './form.mjs';
1
+ export { createFormContext as unstable_createFormContext, updateFieldValue as unstable_updateFieldValue } from './form.mjs';
2
2
  export { isFieldElement } from './dom.mjs';
3
3
  export { INTENT, STATE, parse, serializeIntent } from './submission.mjs';
4
4
  export { formatPaths, getPaths, isPrefix } from './formdata.mjs';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A set of opinionated helpers built on top of the Constraint Validation API",
4
4
  "homepage": "https://conform.guide",
5
5
  "license": "MIT",
6
- "version": "1.3.0",
6
+ "version": "1.5.0",
7
7
  "main": "index.js",
8
8
  "module": "index.mjs",
9
9
  "types": "index.d.ts",