@conform-to/dom
Advanced tools
Comparing version 0.5.1 to 0.6.0-pre.0
@@ -27,2 +27,3 @@ 'use strict'; | ||
function _defineProperty(obj, key, value) { | ||
key = _toPropertyKey(key); | ||
if (key in obj) { | ||
@@ -40,4 +41,20 @@ Object.defineProperty(obj, key, { | ||
} | ||
function _toPrimitive(input, hint) { | ||
if (typeof input !== "object" || input === null) return input; | ||
var prim = input[Symbol.toPrimitive]; | ||
if (prim !== undefined) { | ||
var res = prim.call(input, hint || "default"); | ||
if (typeof res !== "object") return res; | ||
throw new TypeError("@@toPrimitive must return a primitive value."); | ||
} | ||
return (hint === "string" ? String : Number)(input); | ||
} | ||
function _toPropertyKey(arg) { | ||
var key = _toPrimitive(arg, "string"); | ||
return typeof key === "symbol" ? key : String(key); | ||
} | ||
exports.defineProperty = _defineProperty; | ||
exports.objectSpread2 = _objectSpread2; | ||
exports.toPrimitive = _toPrimitive; | ||
exports.toPropertyKey = _toPropertyKey; |
122
index.d.ts
@@ -1,3 +0,3 @@ | ||
export declare type Primitive = null | undefined | string | number | boolean | Date; | ||
export declare type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; | ||
export type Primitive = null | undefined | string | number | boolean | Date; | ||
export type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; | ||
export interface FieldConfig<Schema = unknown> extends FieldConstraint<Schema> { | ||
@@ -7,10 +7,10 @@ id?: string; | ||
defaultValue?: FieldValue<Schema>; | ||
initialError?: Array<[string, string]>; | ||
initialError?: Record<string, string | string[]>; | ||
form?: string; | ||
errorId?: string; | ||
} | ||
export declare type FieldValue<Schema> = Schema extends Primitive ? string : Schema extends File ? File : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : Schema extends Record<string, any> ? { | ||
export type FieldValue<Schema> = Schema extends Primitive ? string : Schema extends File ? File : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : Schema extends Record<string, any> ? { | ||
[Key in keyof Schema]?: FieldValue<Schema[Key]>; | ||
} : unknown; | ||
export declare type FieldConstraint<Schema = any> = { | ||
} : any; | ||
export type FieldConstraint<Schema = any> = { | ||
required?: boolean; | ||
@@ -25,13 +25,18 @@ minLength?: number; | ||
}; | ||
export declare type FieldsetConstraint<Schema extends Record<string, any>> = { | ||
export type FieldsetConstraint<Schema extends Record<string, any>> = { | ||
[Key in keyof Schema]?: FieldConstraint<Schema[Key]>; | ||
}; | ||
export declare type Submission<Schema = unknown> = { | ||
type: string; | ||
intent?: string; | ||
value: FieldValue<Schema>; | ||
error: Array<[string, string]>; | ||
export type Submission<Schema extends Record<string, any> | unknown = unknown> = unknown extends Schema ? { | ||
intent: string; | ||
payload: Record<string, any>; | ||
error: Record<string, string | string[]>; | ||
} : { | ||
intent: string; | ||
payload: Record<string, any>; | ||
value?: Schema; | ||
error: Record<string, string | string[]>; | ||
toJSON(): Submission; | ||
}; | ||
export interface CommandButtonProps<Name extends string = string> { | ||
name: `conform/${Name}`; | ||
export interface IntentButtonProps { | ||
name: '__intent__'; | ||
value: string; | ||
@@ -44,5 +49,15 @@ formNoValidate?: boolean; | ||
export declare function getFormData(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): FormData; | ||
export type FormMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; | ||
export type FormEncType = 'application/x-www-form-urlencoded' | 'multipart/form-data'; | ||
export declare function getFormAttributes(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): { | ||
action: string; | ||
encType: FormEncType; | ||
method: FormMethod; | ||
}; | ||
export declare function getName(paths: Array<string | number>): string; | ||
export declare function shouldValidate(submission: Submission, name: string): boolean; | ||
export declare function hasError(error: Array<[string, string]>, name?: string): boolean; | ||
export declare function shouldValidate(intent: string, name: string): boolean; | ||
export declare function getValidationMessage(errors?: string | string[]): string; | ||
export declare function getErrors(message: string | undefined): string[]; | ||
export declare const VALIDATION_UNDEFINED = "__undefined__"; | ||
export declare const VALIDATION_SKIPPED = "__skipped__"; | ||
export declare function reportSubmission(form: HTMLFormElement, submission: Submission): void; | ||
@@ -57,5 +72,8 @@ export declare function setValue<T>(target: any, paths: Array<string | number>, valueFn: (prev?: T) => T): void; | ||
/** | ||
* Creates a command button on demand and trigger a form submit by clicking it. | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
export declare function requestCommand(form: HTMLFormElement | undefined, buttonProps: CommandButtonProps): void; | ||
export declare function requestIntent(form: HTMLFormElement | undefined, buttonProps: { | ||
value: string; | ||
formNoValidate?: boolean; | ||
}): void; | ||
/** | ||
@@ -66,8 +84,32 @@ * Returns the properties required to configure a command button for validation | ||
*/ | ||
export declare function validate(field?: string): CommandButtonProps<'validate'>; | ||
export declare function validate(field?: string): IntentButtonProps; | ||
export declare function getFormElement(element: HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null): HTMLFormElement | null; | ||
export declare function focus(field: FieldElement): void; | ||
export declare function getSubmissionType(name: string): string | null; | ||
export declare function parse<Schema extends Record<string, any>>(payload: FormData | URLSearchParams): Submission<Schema>; | ||
export declare type ListCommand<Schema = unknown> = { | ||
export declare function parse(payload: FormData | URLSearchParams): Submission; | ||
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: { | ||
resolve?: (payload: Record<string, any>, intent: string) => { | ||
value: Schema; | ||
} | { | ||
error: Record<string, string | string[]>; | ||
}; | ||
}): Submission<Schema>; | ||
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: { | ||
resolve?: (payload: Record<string, any>, intent: string) => Promise<{ | ||
value: Schema; | ||
} | { | ||
error: Record<string, string | string[]>; | ||
}>; | ||
}): Promise<Submission<Schema>>; | ||
export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: { | ||
resolve?: (payload: Record<string, any>, intent: string) => ({ | ||
value: Schema; | ||
} | { | ||
error: Record<string, string | string[]>; | ||
}) | Promise<{ | ||
value: Schema; | ||
} | { | ||
error: Record<string, string | string[]>; | ||
}>; | ||
}): Submission<Schema> | Promise<Submission<Schema>>; | ||
export type ListCommand<Schema = unknown> = { | ||
type: 'prepend'; | ||
@@ -105,23 +147,22 @@ scope: string; | ||
}; | ||
export declare function parseListCommand<Schema = unknown>(data: string): ListCommand<Schema>; | ||
export declare function parseListCommand<Schema = unknown>(intent: string): ListCommand<Schema> | null; | ||
export declare function updateList<Schema>(list: Array<Schema>, command: ListCommand<Schema>): Array<Schema>; | ||
export declare function handleList<Schema>(submission: Submission<Schema>): Submission<Schema>; | ||
export interface ListCommandButtonBuilder { | ||
append<Schema>(name: string, payload?: { | ||
defaultValue: Schema; | ||
}): CommandButtonProps<'list'>; | ||
}): IntentButtonProps; | ||
prepend<Schema>(name: string, payload?: { | ||
defaultValue: Schema; | ||
}): CommandButtonProps<'list'>; | ||
}): IntentButtonProps; | ||
replace<Schema>(name: string, payload: { | ||
defaultValue: Schema; | ||
index: number; | ||
}): CommandButtonProps<'list'>; | ||
}): IntentButtonProps; | ||
remove(name: string, payload: { | ||
index: number; | ||
}): CommandButtonProps<'list'>; | ||
}): IntentButtonProps; | ||
reorder(name: string, payload: { | ||
from: number; | ||
to: number; | ||
}): CommandButtonProps<'list'>; | ||
}): IntentButtonProps; | ||
} | ||
@@ -134,1 +175,24 @@ /** | ||
export declare const list: ListCommandButtonBuilder; | ||
/** | ||
* Validate the form with the Constraint Validation API | ||
* @see https://conform.guide/api/react#validateconstraint | ||
*/ | ||
export declare function validateConstraint(options: { | ||
form: HTMLFormElement; | ||
formData?: FormData; | ||
constraint?: Record<Lowercase<string>, (value: string, context: { | ||
formData: FormData; | ||
attributeValue: string; | ||
}) => boolean>; | ||
acceptMultipleErrors?: ({ name, intent, payload, }: { | ||
name: string; | ||
intent: string; | ||
payload: Record<string, any>; | ||
}) => boolean; | ||
formatMessages?: ({ name, validity, constraint, defaultErrors, }: { | ||
name: string; | ||
validity: ValidityState; | ||
constraint: Record<string, boolean>; | ||
defaultErrors: string[]; | ||
}) => string[]; | ||
}): Submission; |
353
index.js
@@ -7,2 +7,17 @@ 'use strict'; | ||
// type Join<K, P> = P extends string | number ? | ||
// K extends string | number ? | ||
// `${K}${"" extends P ? "" : "."}${P}` | ||
// : never : never; | ||
// type DottedPaths<T> = T extends object ? | ||
// { [K in keyof T]-?: K extends string | number ? | ||
// `${K}` | Join<K, DottedPaths<T[K]>> | ||
// : never | ||
// }[keyof T] : "" | ||
// type Pathfix<T> = T extends `${infer Prefix}.${number}${infer Postfix}` ? `${Prefix}[${number}]${Pathfix<Postfix>}` : T; | ||
// type Path<Schema> = Pathfix<DottedPaths<Schema>> | ''; | ||
function isFieldElement(element) { | ||
@@ -37,2 +52,14 @@ return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON'); | ||
} | ||
function getFormAttributes(form, submitter) { | ||
var _ref, _submitter$getAttribu, _ref2, _submitter$getAttribu2, _submitter$getAttribu3; | ||
var enforce = (value, list) => list.includes(value) ? value : list[0]; | ||
var action = (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search); | ||
var method = (_ref2 = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.getAttribute('method')) !== null && _ref2 !== void 0 ? _ref2 : 'get'; | ||
var encType = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.enctype; | ||
return { | ||
action, | ||
encType: enforce(encType, ['application/x-www-form-urlencoded', 'multipart/form-data']), | ||
method: enforce(method, ['get', 'post', 'put', 'patch', 'delete']) | ||
}; | ||
} | ||
function getName(paths) { | ||
@@ -49,39 +76,52 @@ return paths.reduce((name, path) => { | ||
} | ||
function shouldValidate(submission, name) { | ||
return submission.type === 'submit' || submission.type === 'validate' && (submission.intent === '' || submission.intent === name); | ||
function shouldValidate(intent, name) { | ||
var _parseListCommand; | ||
var [type] = intent.split('/', 1); | ||
switch (type) { | ||
case 'validate': | ||
return intent === 'validate' || intent === "validate/".concat(name); | ||
case 'list': | ||
return ((_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) === name; | ||
default: | ||
return true; | ||
} | ||
} | ||
function hasError(error, name) { | ||
return typeof error.find(_ref => { | ||
var [fieldName, message] = _ref; | ||
return (typeof name === 'undefined' || name === fieldName) && message !== ''; | ||
}) !== 'undefined'; | ||
function getValidationMessage(errors) { | ||
return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31)); | ||
} | ||
function getErrors(message) { | ||
if (!message) { | ||
return []; | ||
} | ||
return message.split(String.fromCharCode(31)); | ||
} | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
function reportSubmission(form, submission) { | ||
var messageByName = new Map(); | ||
for (var [_name, message] of submission.error) { | ||
if (!messageByName.has(_name)) { | ||
// Only keep the first error message (for now) | ||
messageByName.set(_name, message); | ||
for (var [_name, message] of Object.entries(submission.error)) { | ||
// There is no need to create a placeholder button if all we want is to reset the error | ||
if (message === '') { | ||
continue; | ||
} | ||
// We can't use empty string as button name | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var item = form.elements.namedItem(elementName); | ||
if (item instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
throw new Error('Repeated field name is not supported'); | ||
} | ||
// We can't use empty string as button name | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var item = form.elements.namedItem(elementName); | ||
if (item instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
console.warn('Repeated field name is not supported.'); | ||
continue; | ||
} | ||
} | ||
if (item === null) { | ||
// Create placeholder button to keep the error without contributing to the form data | ||
var button = document.createElement('button'); | ||
button.name = elementName; | ||
button.hidden = true; | ||
button.dataset.conformTouched = 'true'; | ||
item = button; | ||
form.appendChild(button); | ||
} | ||
} | ||
if (item === null) { | ||
// Create placeholder button to keep the error without contributing to the form data | ||
var button = document.createElement('button'); | ||
button.name = elementName; | ||
button.hidden = true; | ||
button.dataset.conformTouched = 'true'; | ||
form.appendChild(button); | ||
} | ||
} | ||
@@ -91,12 +131,12 @@ for (var element of form.elements) { | ||
var _elementName = element.name !== '__form__' ? element.name : ''; | ||
var _message = messageByName.get(_elementName); | ||
var elementShouldValidate = shouldValidate(submission, _elementName); | ||
var _message = submission.error[_elementName]; | ||
var elementShouldValidate = shouldValidate(submission.intent, _elementName); | ||
if (elementShouldValidate) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (typeof _message !== 'undefined' || elementShouldValidate) { | ||
if (typeof _message === 'undefined' || ![].concat(_message).includes(VALIDATION_SKIPPED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(_message !== null && _message !== void 0 ? _message : ''); | ||
element.setCustomValidity(getValidationMessage(_message)); | ||
element.dispatchEvent(invalidEvent); | ||
@@ -140,5 +180,5 @@ } | ||
/** | ||
* Creates a command button on demand and trigger a form submit by clicking it. | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
function requestCommand(form, buttonProps) { | ||
function requestIntent(form, buttonProps) { | ||
if (!form) { | ||
@@ -149,3 +189,3 @@ console.warn('No form element is provided'); | ||
var button = document.createElement('button'); | ||
button.name = buttonProps.name; | ||
button.name = '__intent__'; | ||
button.value = buttonProps.value; | ||
@@ -168,4 +208,4 @@ button.hidden = true; | ||
return { | ||
name: 'conform/validate', | ||
value: field !== null && field !== void 0 ? field : '', | ||
name: '__intent__', | ||
value: field ? "validate/".concat(field) : 'validate', | ||
formNoValidate: true | ||
@@ -188,66 +228,76 @@ }; | ||
} | ||
function getSubmissionType(name) { | ||
var prefix = 'conform/'; | ||
if (!name.startsWith(prefix) || name.length <= prefix.length) { | ||
return null; | ||
} | ||
return name.slice(prefix.length); | ||
} | ||
function parse(payload) { | ||
var hasCommand = false; | ||
function parse(payload, options) { | ||
var submission = { | ||
type: 'submit', | ||
value: {}, | ||
error: [] | ||
intent: 'submit', | ||
payload: {}, | ||
error: {} | ||
}; | ||
try { | ||
var _loop = function _loop(value, _name2) { | ||
var submissionType = getSubmissionType(_name2); | ||
if (submissionType) { | ||
if (typeof value !== 'string') { | ||
throw new Error('The conform command could not be used on a file input'); | ||
var _loop = function _loop(_value) { | ||
if (_name2 === '__intent__') { | ||
if (typeof _value !== 'string' || submission.intent !== 'submit') { | ||
throw new Error('The intent could only be set on a button'); | ||
} | ||
submission.intent = _value; | ||
} else { | ||
var _paths = getPaths(_name2); | ||
setValue(submission.payload, _paths, prev => { | ||
if (!prev) { | ||
return _value; | ||
} else if (Array.isArray(prev)) { | ||
return prev.concat(_value); | ||
} else { | ||
return [prev, _value]; | ||
} | ||
if (hasCommand) { | ||
throw new Error('The conform command could only be set on a button'); | ||
} | ||
submission = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission), {}, { | ||
type: submissionType, | ||
intent: value | ||
}); | ||
hasCommand = true; | ||
} else { | ||
var paths = getPaths(_name2); | ||
setValue(submission.value, paths, prev => { | ||
if (!prev) { | ||
return value; | ||
} else if (Array.isArray(prev)) { | ||
return prev.concat(value); | ||
} else { | ||
return [prev, value]; | ||
} | ||
}); | ||
} | ||
}; | ||
for (var [_name2, value] of payload.entries()) { | ||
_loop(value, _name2); | ||
}); | ||
} | ||
switch (submission.type) { | ||
case 'list': | ||
submission = handleList(submission); | ||
break; | ||
} | ||
} catch (e) { | ||
submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']); | ||
}; | ||
for (var [_name2, _value] of payload.entries()) { | ||
_loop(_value); | ||
} | ||
return submission; | ||
var command = parseListCommand(submission.intent); | ||
if (command) { | ||
var paths = getPaths(command.scope); | ||
setValue(submission.payload, paths, list => { | ||
if (typeof list !== 'undefined' && !Array.isArray(list)) { | ||
throw new Error('The list command can only be applied to a list'); | ||
} | ||
return updateList(list !== null && list !== void 0 ? list : [], command); | ||
}); | ||
} | ||
if (typeof (options === null || options === void 0 ? void 0 : options.resolve) === 'undefined') { | ||
return submission; | ||
} | ||
var result = options.resolve(submission.payload, submission.intent); | ||
var mergeResolveResult = resolved => { | ||
var result = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, submission), resolved), {}, { | ||
toJSON() { | ||
return { | ||
intent: this.intent, | ||
payload: this.payload, | ||
error: this.error | ||
}; | ||
} | ||
}); | ||
return result; | ||
}; | ||
if (result instanceof Promise) { | ||
return result.then(mergeResolveResult); | ||
} | ||
return mergeResolveResult(result); | ||
} | ||
function parseListCommand(data) { | ||
function parseListCommand(intent) { | ||
try { | ||
var command = JSON.parse(data); | ||
if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder', 'combine'].includes(command.type)) { | ||
throw new Error("Unknown list command received: ".concat(command.type)); | ||
var [group, type, scope, json] = intent.split('/'); | ||
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) { | ||
return null; | ||
} | ||
return command; | ||
var _payload = JSON.parse(json); | ||
return { | ||
// @ts-expect-error | ||
type, | ||
scope, | ||
payload: _payload | ||
}; | ||
} catch (error) { | ||
throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error)); | ||
return null; | ||
} | ||
@@ -283,17 +333,2 @@ } | ||
} | ||
function handleList(submission) { | ||
var _submission$intent; | ||
if (submission.type !== 'list') { | ||
return submission; | ||
} | ||
var command = parseListCommand((_submission$intent = submission.intent) !== null && _submission$intent !== void 0 ? _submission$intent : ''); | ||
var paths = getPaths(command.scope); | ||
setValue(submission.value, paths, list => { | ||
if (typeof list !== 'undefined' && !Array.isArray(list)) { | ||
throw new Error('The list command can only be applied to a list'); | ||
} | ||
return updateList(list !== null && list !== void 0 ? list : [], command); | ||
}); | ||
return submission; | ||
} | ||
/** | ||
@@ -315,8 +350,4 @@ * Helpers to configure a command button for modifying a list | ||
return { | ||
name: 'conform/list', | ||
value: JSON.stringify({ | ||
type, | ||
scope, | ||
payload | ||
}), | ||
name: '__intent__', | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
formNoValidate: true | ||
@@ -329,3 +360,88 @@ }; | ||
/** | ||
* Validate the form with the Constraint Validation API | ||
* @see https://conform.guide/api/react#validateconstraint | ||
*/ | ||
function validateConstraint(options) { | ||
var _options$formData, _options$formatMessag; | ||
var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form); | ||
var getDefaultErrors = (validity, result) => { | ||
var errors = []; | ||
if (validity.valueMissing) errors.push('required'); | ||
if (validity.typeMismatch || validity.badInput) errors.push('type'); | ||
if (validity.tooShort) errors.push('minLength'); | ||
if (validity.rangeUnderflow) errors.push('min'); | ||
if (validity.stepMismatch) errors.push('step'); | ||
if (validity.tooLong) errors.push('maxLength'); | ||
if (validity.rangeOverflow) errors.push('max'); | ||
if (validity.patternMismatch) errors.push('pattern'); | ||
for (var [constraintName, valid] of Object.entries(result)) { | ||
if (!valid) { | ||
errors.push(constraintName); | ||
} | ||
} | ||
return errors; | ||
}; | ||
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref3 => { | ||
var { | ||
defaultErrors | ||
} = _ref3; | ||
return defaultErrors; | ||
}; | ||
return parse(formData, { | ||
resolve(payload, intent) { | ||
var error = {}; | ||
var constraintPattern = /^constraint[A-Z][^A-Z]*$/; | ||
var _loop2 = function _loop2(element) { | ||
if (isFieldElement(element)) { | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var _name3 = element.name === '__form__' ? '' : element.name; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => { | ||
var [name, attributeValue = ''] = _ref4; | ||
if (constraintPattern.test(name)) { | ||
var _options$constraint; | ||
var constraintName = name.slice(10).toLowerCase(); | ||
var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName]; | ||
if (typeof _validate === 'function') { | ||
result[constraintName] = _validate(element.value, { | ||
formData, | ||
attributeValue | ||
}); | ||
} else { | ||
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API.")); | ||
} | ||
} | ||
return result; | ||
}, {}); | ||
var errors = formatMessages({ | ||
name: _name3, | ||
validity: element.validity, | ||
constraint, | ||
defaultErrors: getDefaultErrors(element.validity, constraint) | ||
}); | ||
var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, { | ||
name: _name3, | ||
payload, | ||
intent | ||
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false; | ||
if (errors.length > 0) { | ||
error[_name3] = shouldAcceptMultipleErrors ? errors : errors[0]; | ||
} | ||
} | ||
}; | ||
for (var element of options.form.elements) { | ||
_loop2(element); | ||
} | ||
return { | ||
error | ||
}; | ||
} | ||
}); | ||
} | ||
exports.VALIDATION_SKIPPED = VALIDATION_SKIPPED; | ||
exports.VALIDATION_UNDEFINED = VALIDATION_UNDEFINED; | ||
exports.focus = focus; | ||
exports.getErrors = getErrors; | ||
exports.getFormAttributes = getFormAttributes; | ||
exports.getFormData = getFormData; | ||
@@ -336,5 +452,3 @@ exports.getFormElement = getFormElement; | ||
exports.getPaths = getPaths; | ||
exports.getSubmissionType = getSubmissionType; | ||
exports.handleList = handleList; | ||
exports.hasError = hasError; | ||
exports.getValidationMessage = getValidationMessage; | ||
exports.isFieldElement = isFieldElement; | ||
@@ -345,3 +459,3 @@ exports.list = list; | ||
exports.reportSubmission = reportSubmission; | ||
exports.requestCommand = requestCommand; | ||
exports.requestIntent = requestIntent; | ||
exports.requestSubmit = requestSubmit; | ||
@@ -352,1 +466,2 @@ exports.setValue = setValue; | ||
exports.validate = validate; | ||
exports.validateConstraint = validateConstraint; |
@@ -23,2 +23,3 @@ function ownKeys(object, enumerableOnly) { | ||
function _defineProperty(obj, key, value) { | ||
key = _toPropertyKey(key); | ||
if (key in obj) { | ||
@@ -36,3 +37,17 @@ Object.defineProperty(obj, key, { | ||
} | ||
function _toPrimitive(input, hint) { | ||
if (typeof input !== "object" || input === null) return input; | ||
var prim = input[Symbol.toPrimitive]; | ||
if (prim !== undefined) { | ||
var res = prim.call(input, hint || "default"); | ||
if (typeof res !== "object") return res; | ||
throw new TypeError("@@toPrimitive must return a primitive value."); | ||
} | ||
return (hint === "string" ? String : Number)(input); | ||
} | ||
function _toPropertyKey(arg) { | ||
var key = _toPrimitive(arg, "string"); | ||
return typeof key === "symbol" ? key : String(key); | ||
} | ||
export { _defineProperty as defineProperty, _objectSpread2 as objectSpread2 }; | ||
export { _defineProperty as defineProperty, _objectSpread2 as objectSpread2, _toPrimitive as toPrimitive, _toPropertyKey as toPropertyKey }; |
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
// type Join<K, P> = P extends string | number ? | ||
// K extends string | number ? | ||
// `${K}${"" extends P ? "" : "."}${P}` | ||
// : never : never; | ||
// type DottedPaths<T> = T extends object ? | ||
// { [K in keyof T]-?: K extends string | number ? | ||
// `${K}` | Join<K, DottedPaths<T[K]>> | ||
// : never | ||
// }[keyof T] : "" | ||
// type Pathfix<T> = T extends `${infer Prefix}.${number}${infer Postfix}` ? `${Prefix}[${number}]${Pathfix<Postfix>}` : T; | ||
// type Path<Schema> = Pathfix<DottedPaths<Schema>> | ''; | ||
function isFieldElement(element) { | ||
@@ -32,2 +47,14 @@ return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON'); | ||
} | ||
function getFormAttributes(form, submitter) { | ||
var _ref, _submitter$getAttribu, _ref2, _submitter$getAttribu2, _submitter$getAttribu3; | ||
var enforce = (value, list) => list.includes(value) ? value : list[0]; | ||
var action = (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search); | ||
var method = (_ref2 = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.getAttribute('method')) !== null && _ref2 !== void 0 ? _ref2 : 'get'; | ||
var encType = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.enctype; | ||
return { | ||
action, | ||
encType: enforce(encType, ['application/x-www-form-urlencoded', 'multipart/form-data']), | ||
method: enforce(method, ['get', 'post', 'put', 'patch', 'delete']) | ||
}; | ||
} | ||
function getName(paths) { | ||
@@ -44,39 +71,52 @@ return paths.reduce((name, path) => { | ||
} | ||
function shouldValidate(submission, name) { | ||
return submission.type === 'submit' || submission.type === 'validate' && (submission.intent === '' || submission.intent === name); | ||
function shouldValidate(intent, name) { | ||
var _parseListCommand; | ||
var [type] = intent.split('/', 1); | ||
switch (type) { | ||
case 'validate': | ||
return intent === 'validate' || intent === "validate/".concat(name); | ||
case 'list': | ||
return ((_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) === name; | ||
default: | ||
return true; | ||
} | ||
} | ||
function hasError(error, name) { | ||
return typeof error.find(_ref => { | ||
var [fieldName, message] = _ref; | ||
return (typeof name === 'undefined' || name === fieldName) && message !== ''; | ||
}) !== 'undefined'; | ||
function getValidationMessage(errors) { | ||
return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31)); | ||
} | ||
function getErrors(message) { | ||
if (!message) { | ||
return []; | ||
} | ||
return message.split(String.fromCharCode(31)); | ||
} | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
function reportSubmission(form, submission) { | ||
var messageByName = new Map(); | ||
for (var [_name, message] of submission.error) { | ||
if (!messageByName.has(_name)) { | ||
// Only keep the first error message (for now) | ||
messageByName.set(_name, message); | ||
for (var [_name, message] of Object.entries(submission.error)) { | ||
// There is no need to create a placeholder button if all we want is to reset the error | ||
if (message === '') { | ||
continue; | ||
} | ||
// We can't use empty string as button name | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var item = form.elements.namedItem(elementName); | ||
if (item instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
throw new Error('Repeated field name is not supported'); | ||
} | ||
// We can't use empty string as button name | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var item = form.elements.namedItem(elementName); | ||
if (item instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
console.warn('Repeated field name is not supported.'); | ||
continue; | ||
} | ||
} | ||
if (item === null) { | ||
// Create placeholder button to keep the error without contributing to the form data | ||
var button = document.createElement('button'); | ||
button.name = elementName; | ||
button.hidden = true; | ||
button.dataset.conformTouched = 'true'; | ||
item = button; | ||
form.appendChild(button); | ||
} | ||
} | ||
if (item === null) { | ||
// Create placeholder button to keep the error without contributing to the form data | ||
var button = document.createElement('button'); | ||
button.name = elementName; | ||
button.hidden = true; | ||
button.dataset.conformTouched = 'true'; | ||
form.appendChild(button); | ||
} | ||
} | ||
@@ -86,12 +126,12 @@ for (var element of form.elements) { | ||
var _elementName = element.name !== '__form__' ? element.name : ''; | ||
var _message = messageByName.get(_elementName); | ||
var elementShouldValidate = shouldValidate(submission, _elementName); | ||
var _message = submission.error[_elementName]; | ||
var elementShouldValidate = shouldValidate(submission.intent, _elementName); | ||
if (elementShouldValidate) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (typeof _message !== 'undefined' || elementShouldValidate) { | ||
if (typeof _message === 'undefined' || ![].concat(_message).includes(VALIDATION_SKIPPED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(_message !== null && _message !== void 0 ? _message : ''); | ||
element.setCustomValidity(getValidationMessage(_message)); | ||
element.dispatchEvent(invalidEvent); | ||
@@ -135,5 +175,5 @@ } | ||
/** | ||
* Creates a command button on demand and trigger a form submit by clicking it. | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
function requestCommand(form, buttonProps) { | ||
function requestIntent(form, buttonProps) { | ||
if (!form) { | ||
@@ -144,3 +184,3 @@ console.warn('No form element is provided'); | ||
var button = document.createElement('button'); | ||
button.name = buttonProps.name; | ||
button.name = '__intent__'; | ||
button.value = buttonProps.value; | ||
@@ -163,4 +203,4 @@ button.hidden = true; | ||
return { | ||
name: 'conform/validate', | ||
value: field !== null && field !== void 0 ? field : '', | ||
name: '__intent__', | ||
value: field ? "validate/".concat(field) : 'validate', | ||
formNoValidate: true | ||
@@ -183,66 +223,76 @@ }; | ||
} | ||
function getSubmissionType(name) { | ||
var prefix = 'conform/'; | ||
if (!name.startsWith(prefix) || name.length <= prefix.length) { | ||
return null; | ||
} | ||
return name.slice(prefix.length); | ||
} | ||
function parse(payload) { | ||
var hasCommand = false; | ||
function parse(payload, options) { | ||
var submission = { | ||
type: 'submit', | ||
value: {}, | ||
error: [] | ||
intent: 'submit', | ||
payload: {}, | ||
error: {} | ||
}; | ||
try { | ||
var _loop = function _loop(value, _name2) { | ||
var submissionType = getSubmissionType(_name2); | ||
if (submissionType) { | ||
if (typeof value !== 'string') { | ||
throw new Error('The conform command could not be used on a file input'); | ||
var _loop = function _loop(_value) { | ||
if (_name2 === '__intent__') { | ||
if (typeof _value !== 'string' || submission.intent !== 'submit') { | ||
throw new Error('The intent could only be set on a button'); | ||
} | ||
submission.intent = _value; | ||
} else { | ||
var _paths = getPaths(_name2); | ||
setValue(submission.payload, _paths, prev => { | ||
if (!prev) { | ||
return _value; | ||
} else if (Array.isArray(prev)) { | ||
return prev.concat(_value); | ||
} else { | ||
return [prev, _value]; | ||
} | ||
if (hasCommand) { | ||
throw new Error('The conform command could only be set on a button'); | ||
} | ||
submission = _objectSpread2(_objectSpread2({}, submission), {}, { | ||
type: submissionType, | ||
intent: value | ||
}); | ||
hasCommand = true; | ||
} else { | ||
var paths = getPaths(_name2); | ||
setValue(submission.value, paths, prev => { | ||
if (!prev) { | ||
return value; | ||
} else if (Array.isArray(prev)) { | ||
return prev.concat(value); | ||
} else { | ||
return [prev, value]; | ||
} | ||
}); | ||
} | ||
}; | ||
for (var [_name2, value] of payload.entries()) { | ||
_loop(value, _name2); | ||
}); | ||
} | ||
switch (submission.type) { | ||
case 'list': | ||
submission = handleList(submission); | ||
break; | ||
} | ||
} catch (e) { | ||
submission.error.push(['', e instanceof Error ? e.message : 'Invalid payload received']); | ||
}; | ||
for (var [_name2, _value] of payload.entries()) { | ||
_loop(_value); | ||
} | ||
return submission; | ||
var command = parseListCommand(submission.intent); | ||
if (command) { | ||
var paths = getPaths(command.scope); | ||
setValue(submission.payload, paths, list => { | ||
if (typeof list !== 'undefined' && !Array.isArray(list)) { | ||
throw new Error('The list command can only be applied to a list'); | ||
} | ||
return updateList(list !== null && list !== void 0 ? list : [], command); | ||
}); | ||
} | ||
if (typeof (options === null || options === void 0 ? void 0 : options.resolve) === 'undefined') { | ||
return submission; | ||
} | ||
var result = options.resolve(submission.payload, submission.intent); | ||
var mergeResolveResult = resolved => { | ||
var result = _objectSpread2(_objectSpread2(_objectSpread2({}, submission), resolved), {}, { | ||
toJSON() { | ||
return { | ||
intent: this.intent, | ||
payload: this.payload, | ||
error: this.error | ||
}; | ||
} | ||
}); | ||
return result; | ||
}; | ||
if (result instanceof Promise) { | ||
return result.then(mergeResolveResult); | ||
} | ||
return mergeResolveResult(result); | ||
} | ||
function parseListCommand(data) { | ||
function parseListCommand(intent) { | ||
try { | ||
var command = JSON.parse(data); | ||
if (typeof command.type !== 'string' || !['prepend', 'append', 'replace', 'remove', 'reorder', 'combine'].includes(command.type)) { | ||
throw new Error("Unknown list command received: ".concat(command.type)); | ||
var [group, type, scope, json] = intent.split('/'); | ||
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) { | ||
return null; | ||
} | ||
return command; | ||
var _payload = JSON.parse(json); | ||
return { | ||
// @ts-expect-error | ||
type, | ||
scope, | ||
payload: _payload | ||
}; | ||
} catch (error) { | ||
throw new Error("Invalid list command: \"".concat(data, "\"; ").concat(error)); | ||
return null; | ||
} | ||
@@ -278,17 +328,2 @@ } | ||
} | ||
function handleList(submission) { | ||
var _submission$intent; | ||
if (submission.type !== 'list') { | ||
return submission; | ||
} | ||
var command = parseListCommand((_submission$intent = submission.intent) !== null && _submission$intent !== void 0 ? _submission$intent : ''); | ||
var paths = getPaths(command.scope); | ||
setValue(submission.value, paths, list => { | ||
if (typeof list !== 'undefined' && !Array.isArray(list)) { | ||
throw new Error('The list command can only be applied to a list'); | ||
} | ||
return updateList(list !== null && list !== void 0 ? list : [], command); | ||
}); | ||
return submission; | ||
} | ||
/** | ||
@@ -310,8 +345,4 @@ * Helpers to configure a command button for modifying a list | ||
return { | ||
name: 'conform/list', | ||
value: JSON.stringify({ | ||
type, | ||
scope, | ||
payload | ||
}), | ||
name: '__intent__', | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
formNoValidate: true | ||
@@ -324,2 +355,83 @@ }; | ||
export { focus, getFormData, getFormElement, getFormElements, getName, getPaths, getSubmissionType, handleList, hasError, isFieldElement, list, parse, parseListCommand, reportSubmission, requestCommand, requestSubmit, setValue, shouldValidate, updateList, validate }; | ||
/** | ||
* Validate the form with the Constraint Validation API | ||
* @see https://conform.guide/api/react#validateconstraint | ||
*/ | ||
function validateConstraint(options) { | ||
var _options$formData, _options$formatMessag; | ||
var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form); | ||
var getDefaultErrors = (validity, result) => { | ||
var errors = []; | ||
if (validity.valueMissing) errors.push('required'); | ||
if (validity.typeMismatch || validity.badInput) errors.push('type'); | ||
if (validity.tooShort) errors.push('minLength'); | ||
if (validity.rangeUnderflow) errors.push('min'); | ||
if (validity.stepMismatch) errors.push('step'); | ||
if (validity.tooLong) errors.push('maxLength'); | ||
if (validity.rangeOverflow) errors.push('max'); | ||
if (validity.patternMismatch) errors.push('pattern'); | ||
for (var [constraintName, valid] of Object.entries(result)) { | ||
if (!valid) { | ||
errors.push(constraintName); | ||
} | ||
} | ||
return errors; | ||
}; | ||
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref3 => { | ||
var { | ||
defaultErrors | ||
} = _ref3; | ||
return defaultErrors; | ||
}; | ||
return parse(formData, { | ||
resolve(payload, intent) { | ||
var error = {}; | ||
var constraintPattern = /^constraint[A-Z][^A-Z]*$/; | ||
var _loop2 = function _loop2(element) { | ||
if (isFieldElement(element)) { | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var _name3 = element.name === '__form__' ? '' : element.name; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => { | ||
var [name, attributeValue = ''] = _ref4; | ||
if (constraintPattern.test(name)) { | ||
var _options$constraint; | ||
var constraintName = name.slice(10).toLowerCase(); | ||
var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName]; | ||
if (typeof _validate === 'function') { | ||
result[constraintName] = _validate(element.value, { | ||
formData, | ||
attributeValue | ||
}); | ||
} else { | ||
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API.")); | ||
} | ||
} | ||
return result; | ||
}, {}); | ||
var errors = formatMessages({ | ||
name: _name3, | ||
validity: element.validity, | ||
constraint, | ||
defaultErrors: getDefaultErrors(element.validity, constraint) | ||
}); | ||
var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, { | ||
name: _name3, | ||
payload, | ||
intent | ||
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false; | ||
if (errors.length > 0) { | ||
error[_name3] = shouldAcceptMultipleErrors ? errors : errors[0]; | ||
} | ||
} | ||
}; | ||
for (var element of options.form.elements) { | ||
_loop2(element); | ||
} | ||
return { | ||
error | ||
}; | ||
} | ||
}); | ||
} | ||
export { VALIDATION_SKIPPED, VALIDATION_UNDEFINED, focus, getErrors, getFormAttributes, getFormData, getFormElement, getFormElements, getName, getPaths, getValidationMessage, isFieldElement, list, parse, parseListCommand, reportSubmission, requestIntent, requestSubmit, setValue, shouldValidate, updateList, validate, validateConstraint }; |
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "0.5.1", | ||
"version": "0.6.0-pre.0", | ||
"main": "index.js", | ||
@@ -8,0 +8,0 @@ "module": "module/index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45150
1155
1
27