@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 +36 -0
- package/form.d.ts +13 -0
- package/form.js +156 -4
- package/form.mjs +157 -6
- package/formdata.d.ts +5 -0
- package/formdata.js +14 -0
- package/formdata.mjs +14 -1
- package/index.d.ts +1 -1
- package/index.js +1 -0
- package/index.mjs +1 -1
- package/package.json +1 -1
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,
|
|
11
|
+
function createFormMeta(options, isResetting) {
|
|
12
12
|
var _lastResult$initialVa, _options$constraint, _lastResult$state$val, _lastResult$state, _ref;
|
|
13
|
-
var lastResult = !
|
|
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: !
|
|
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,
|
|
7
|
+
function createFormMeta(options, isResetting) {
|
|
8
8
|
var _lastResult$initialVa, _options$constraint, _lastResult$state$val, _lastResult$state, _ref;
|
|
9
|
-
var lastResult = !
|
|
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: !
|
|
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
|
-
|
|
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.
|
|
6
|
+
"version": "1.5.0",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"module": "index.mjs",
|
|
9
9
|
"types": "index.d.ts",
|