@conform-to/dom
Advanced tools
Comparing version 0.6.1 to 0.6.2
208
index.d.ts
@@ -1,203 +0,5 @@ | ||
export type Primitive = null | undefined | string | number | boolean | Date; | ||
export type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; | ||
export interface FieldConfig<Schema = unknown> extends FieldConstraint<Schema> { | ||
id?: string; | ||
name: string; | ||
defaultValue?: FieldValue<Schema>; | ||
initialError?: Record<string, string | string[]>; | ||
form?: string; | ||
descriptionId?: string; | ||
errorId?: string; | ||
/** | ||
* The frist error of the field | ||
*/ | ||
error?: string; | ||
/** | ||
* All of the field errors | ||
*/ | ||
errors?: string[]; | ||
} | ||
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]>; | ||
} : any; | ||
export type FieldConstraint<Schema = any> = { | ||
required?: boolean; | ||
minLength?: number; | ||
maxLength?: number; | ||
min?: Schema extends number ? number : string | number; | ||
max?: Schema extends number ? number : string | number; | ||
step?: Schema extends number ? number : string | number; | ||
multiple?: boolean; | ||
pattern?: string; | ||
}; | ||
export type FieldsetConstraint<Schema extends Record<string, any>> = { | ||
[Key in keyof Schema]?: FieldConstraint<Schema[Key]>; | ||
}; | ||
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 IntentButtonProps { | ||
name: typeof INTENT; | ||
value: string; | ||
formNoValidate?: boolean; | ||
} | ||
/** | ||
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_) | ||
*/ | ||
export declare function isFieldElement(element: unknown): element is FieldElement; | ||
/** | ||
* Find the corresponding paths based on the formatted name | ||
* @param name formatted name | ||
* @returns paths | ||
*/ | ||
export declare function getPaths(name: string): Array<string | number>; | ||
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 getScope(intent: string): string | null; | ||
export declare function isFocusedOnIntentButton(form: HTMLFormElement, intent: string): boolean; | ||
export declare function getValidationMessage(errors?: string | string[]): string; | ||
export declare function getErrors(message: string | undefined): string[]; | ||
export declare const FORM_ERROR_ELEMENT_NAME = "__form__"; | ||
export declare const INTENT = "__intent__"; | ||
export declare const VALIDATION_UNDEFINED = "__undefined__"; | ||
export declare const VALIDATION_SKIPPED = "__skipped__"; | ||
export declare function reportSubmission(form: HTMLFormElement, submission: Submission): void; | ||
export declare function setValue<T>(target: any, paths: Array<string | number>, valueFn: (prev?: T) => T): void; | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
export declare function requestIntent(form: HTMLFormElement | undefined, buttonProps: { | ||
value: string; | ||
formNoValidate?: boolean; | ||
}): void; | ||
/** | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
* @see https://conform.guide/api/react#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 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'; | ||
scope: string; | ||
payload: { | ||
defaultValue: Schema; | ||
}; | ||
} | { | ||
type: 'append'; | ||
scope: string; | ||
payload: { | ||
defaultValue: Schema; | ||
}; | ||
} | { | ||
type: 'replace'; | ||
scope: string; | ||
payload: { | ||
defaultValue: Schema; | ||
index: number; | ||
}; | ||
} | { | ||
type: 'remove'; | ||
scope: string; | ||
payload: { | ||
index: number; | ||
}; | ||
} | { | ||
type: 'reorder'; | ||
scope: string; | ||
payload: { | ||
from: number; | ||
to: number; | ||
}; | ||
}; | ||
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 interface ListCommandButtonBuilder { | ||
append<Schema>(name: string, payload?: { | ||
defaultValue: Schema; | ||
}): IntentButtonProps; | ||
prepend<Schema>(name: string, payload?: { | ||
defaultValue: Schema; | ||
}): IntentButtonProps; | ||
replace<Schema>(name: string, payload: { | ||
defaultValue: Schema; | ||
index: number; | ||
}): IntentButtonProps; | ||
remove(name: string, payload: { | ||
index: number; | ||
}): IntentButtonProps; | ||
reorder(name: string, payload: { | ||
from: number; | ||
to: number; | ||
}): IntentButtonProps; | ||
} | ||
/** | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
* @see https://conform.guide/api/react#list | ||
*/ | ||
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; | ||
export { type FormControl as FieldElement, isFormControl as isFieldElement, isFocusableFormControl, getFormAction, getFormControls, getFormElement, getFormEncType, getFormMethod, focusFirstInvalidControl, focusFormControl, createSubmitter, requestSubmit, } from './dom'; | ||
export { formatPaths as getName, getPaths, getFormData, getValidationMessage, getErrors, } from './formdata'; | ||
export { type ListCommand, INTENT, getScope, isSubmitting, validate, list, parseListCommand, updateList, requestIntent, } from './intent'; | ||
export { type Submission, parse } from './parse'; | ||
export { type FieldConstraint, type FieldsetConstraint, type ResolveType, type KeysOf, } from './types'; |
456
index.js
@@ -5,431 +5,33 @@ 'use strict'; | ||
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js'); | ||
var dom = require('./dom.js'); | ||
var formdata = require('./formdata.js'); | ||
var intent = require('./intent.js'); | ||
var parse = require('./parse.js'); | ||
/** | ||
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_) | ||
*/ | ||
function isFieldElement(element) { | ||
return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON'); | ||
} | ||
/** | ||
* Find the corresponding paths based on the formatted name | ||
* @param name formatted name | ||
* @returns paths | ||
*/ | ||
function getPaths(name) { | ||
var pattern = /(\w*)\[(\d+)\]/; | ||
if (!name) { | ||
return []; | ||
} | ||
return name.split('.').flatMap(key => { | ||
var matches = pattern.exec(key); | ||
if (!matches) { | ||
return key; | ||
} | ||
if (matches[1] === '') { | ||
return Number(matches[2]); | ||
} | ||
return [matches[1], Number(matches[2])]; | ||
}); | ||
} | ||
function getFormData(form, submitter) { | ||
var payload = new FormData(form); | ||
if (submitter !== null && submitter !== void 0 && submitter.name) { | ||
payload.append(submitter.name, submitter.value); | ||
} | ||
return payload; | ||
} | ||
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) { | ||
return paths.reduce((name, path) => { | ||
if (typeof path === 'number') { | ||
return "".concat(name, "[").concat(path, "]"); | ||
} | ||
if (name === '' || path === '') { | ||
return [name, path].join(''); | ||
} | ||
return [name, path].join('.'); | ||
}, ''); | ||
} | ||
function getScope(intent) { | ||
var _parseListCommand$sco, _parseListCommand; | ||
var [type, ...rest] = intent.split('/'); | ||
switch (type) { | ||
case 'validate': | ||
return rest.length > 0 ? rest.join('/') : null; | ||
case 'list': | ||
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null; | ||
default: | ||
return null; | ||
} | ||
} | ||
function isFocusedOnIntentButton(form, intent) { | ||
var element = document.activeElement; | ||
return isFieldElement(element) && element.tagName === 'BUTTON' && element.form === form && element.name === INTENT && element.value === intent; | ||
} | ||
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 FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
var INTENT = '__intent__'; | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
function reportSubmission(form, submission) { | ||
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_ERROR_ELEMENT_NAME; | ||
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'; | ||
form.appendChild(button); | ||
} | ||
} | ||
var focusedFirstInvalidField = false; | ||
var scope = getScope(submission.intent); | ||
var isSubmitting = submission.intent.slice(0, submission.intent.indexOf('/')) !== 'validate' && parseListCommand(submission.intent) === null; | ||
for (var element of form.elements) { | ||
if (isFieldElement(element) && element.willValidate) { | ||
var _submission$error$_el; | ||
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var messages = [].concat((_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : []); | ||
var shouldValidate = scope === null || scope === _elementName; | ||
if (shouldValidate) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) { | ||
element.focus(); | ||
focusedFirstInvalidField = true; | ||
} | ||
} | ||
} | ||
} | ||
function setValue(target, paths, valueFn) { | ||
var length = paths.length; | ||
var lastIndex = length - 1; | ||
var index = -1; | ||
var pointer = target; | ||
while (pointer != null && ++index < length) { | ||
var _pointer$key; | ||
var key = paths[index]; | ||
var next = paths[index + 1]; | ||
var newValue = index != lastIndex ? (_pointer$key = pointer[key]) !== null && _pointer$key !== void 0 ? _pointer$key : typeof next === 'number' ? [] : {} : valueFn(pointer[key]); | ||
pointer[key] = newValue; | ||
pointer = pointer[key]; | ||
} | ||
} | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
function requestIntent(form, buttonProps) { | ||
if (!form) { | ||
console.warn('No form element is provided'); | ||
return; | ||
} | ||
var button = document.createElement('button'); | ||
button.name = INTENT; | ||
button.value = buttonProps.value; | ||
button.hidden = true; | ||
if (buttonProps.formNoValidate) { | ||
button.formNoValidate = true; | ||
} | ||
form.appendChild(button); | ||
button.click(); | ||
form.removeChild(button); | ||
} | ||
/** | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
* @see https://conform.guide/api/react#validate | ||
*/ | ||
function validate(field) { | ||
return { | ||
name: INTENT, | ||
value: field ? "validate/".concat(field) : 'validate', | ||
formNoValidate: true | ||
}; | ||
} | ||
function getFormElement(element) { | ||
var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form; | ||
if (!form) { | ||
return null; | ||
} | ||
return form; | ||
} | ||
function parse(payload, options) { | ||
var submission = { | ||
intent: 'submit', | ||
payload: {}, | ||
error: {} | ||
}; | ||
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]; | ||
} | ||
}); | ||
} | ||
}; | ||
for (var [_name2, _value] of payload.entries()) { | ||
_loop(_value); | ||
} | ||
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(intent) { | ||
try { | ||
var [group, type, scope, json] = intent.split('/'); | ||
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) { | ||
return null; | ||
} | ||
var _payload = JSON.parse(json); | ||
return { | ||
// @ts-expect-error | ||
type, | ||
scope, | ||
payload: _payload | ||
}; | ||
} catch (error) { | ||
return null; | ||
} | ||
} | ||
function updateList(list, command) { | ||
switch (command.type) { | ||
case 'prepend': | ||
{ | ||
list.unshift(command.payload.defaultValue); | ||
break; | ||
} | ||
case 'append': | ||
{ | ||
list.push(command.payload.defaultValue); | ||
break; | ||
} | ||
case 'replace': | ||
{ | ||
list.splice(command.payload.index, 1, command.payload.defaultValue); | ||
break; | ||
} | ||
case 'remove': | ||
list.splice(command.payload.index, 1); | ||
break; | ||
case 'reorder': | ||
list.splice(command.payload.to, 0, ...list.splice(command.payload.from, 1)); | ||
break; | ||
default: | ||
throw new Error('Unknown list command received'); | ||
} | ||
return list; | ||
} | ||
/** | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
* @see https://conform.guide/api/react#list | ||
*/ | ||
var list = new Proxy({}, { | ||
get(_target, type) { | ||
switch (type) { | ||
case 'append': | ||
case 'prepend': | ||
case 'replace': | ||
case 'remove': | ||
case 'reorder': | ||
return function (scope) { | ||
var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
return { | ||
name: INTENT, | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
formNoValidate: true | ||
}; | ||
}; | ||
} | ||
} | ||
}); | ||
/** | ||
* 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_ERROR_ELEMENT_NAME ? 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.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME; | ||
exports.INTENT = INTENT; | ||
exports.VALIDATION_SKIPPED = VALIDATION_SKIPPED; | ||
exports.VALIDATION_UNDEFINED = VALIDATION_UNDEFINED; | ||
exports.getErrors = getErrors; | ||
exports.getFormAttributes = getFormAttributes; | ||
exports.getFormData = getFormData; | ||
exports.getFormElement = getFormElement; | ||
exports.getName = getName; | ||
exports.getPaths = getPaths; | ||
exports.getScope = getScope; | ||
exports.getValidationMessage = getValidationMessage; | ||
exports.isFieldElement = isFieldElement; | ||
exports.isFocusedOnIntentButton = isFocusedOnIntentButton; | ||
exports.list = list; | ||
exports.parse = parse; | ||
exports.parseListCommand = parseListCommand; | ||
exports.reportSubmission = reportSubmission; | ||
exports.requestIntent = requestIntent; | ||
exports.setValue = setValue; | ||
exports.updateList = updateList; | ||
exports.validate = validate; | ||
exports.validateConstraint = validateConstraint; | ||
exports.createSubmitter = dom.createSubmitter; | ||
exports.focusFirstInvalidControl = dom.focusFirstInvalidControl; | ||
exports.focusFormControl = dom.focusFormControl; | ||
exports.getFormAction = dom.getFormAction; | ||
exports.getFormControls = dom.getFormControls; | ||
exports.getFormElement = dom.getFormElement; | ||
exports.getFormEncType = dom.getFormEncType; | ||
exports.getFormMethod = dom.getFormMethod; | ||
exports.isFieldElement = dom.isFormControl; | ||
exports.isFocusableFormControl = dom.isFocusableFormControl; | ||
exports.requestSubmit = dom.requestSubmit; | ||
exports.getErrors = formdata.getErrors; | ||
exports.getFormData = formdata.getFormData; | ||
exports.getName = formdata.formatPaths; | ||
exports.getPaths = formdata.getPaths; | ||
exports.getValidationMessage = formdata.getValidationMessage; | ||
exports.INTENT = intent.INTENT; | ||
exports.getScope = intent.getScope; | ||
exports.isSubmitting = intent.isSubmitting; | ||
exports.list = intent.list; | ||
exports.parseListCommand = intent.parseListCommand; | ||
exports.requestIntent = intent.requestIntent; | ||
exports.updateList = intent.updateList; | ||
exports.validate = intent.validate; | ||
exports.parse = parse.parse; |
@@ -1,408 +0,4 @@ | ||
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
/** | ||
* Check if the provided reference is a form element (_input_ / _select_ / _textarea_ or _button_) | ||
*/ | ||
function isFieldElement(element) { | ||
return element instanceof Element && (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON'); | ||
} | ||
/** | ||
* Find the corresponding paths based on the formatted name | ||
* @param name formatted name | ||
* @returns paths | ||
*/ | ||
function getPaths(name) { | ||
var pattern = /(\w*)\[(\d+)\]/; | ||
if (!name) { | ||
return []; | ||
} | ||
return name.split('.').flatMap(key => { | ||
var matches = pattern.exec(key); | ||
if (!matches) { | ||
return key; | ||
} | ||
if (matches[1] === '') { | ||
return Number(matches[2]); | ||
} | ||
return [matches[1], Number(matches[2])]; | ||
}); | ||
} | ||
function getFormData(form, submitter) { | ||
var payload = new FormData(form); | ||
if (submitter !== null && submitter !== void 0 && submitter.name) { | ||
payload.append(submitter.name, submitter.value); | ||
} | ||
return payload; | ||
} | ||
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) { | ||
return paths.reduce((name, path) => { | ||
if (typeof path === 'number') { | ||
return "".concat(name, "[").concat(path, "]"); | ||
} | ||
if (name === '' || path === '') { | ||
return [name, path].join(''); | ||
} | ||
return [name, path].join('.'); | ||
}, ''); | ||
} | ||
function getScope(intent) { | ||
var _parseListCommand$sco, _parseListCommand; | ||
var [type, ...rest] = intent.split('/'); | ||
switch (type) { | ||
case 'validate': | ||
return rest.length > 0 ? rest.join('/') : null; | ||
case 'list': | ||
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null; | ||
default: | ||
return null; | ||
} | ||
} | ||
function isFocusedOnIntentButton(form, intent) { | ||
var element = document.activeElement; | ||
return isFieldElement(element) && element.tagName === 'BUTTON' && element.form === form && element.name === INTENT && element.value === intent; | ||
} | ||
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 FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
var INTENT = '__intent__'; | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
function reportSubmission(form, submission) { | ||
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_ERROR_ELEMENT_NAME; | ||
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'; | ||
form.appendChild(button); | ||
} | ||
} | ||
var focusedFirstInvalidField = false; | ||
var scope = getScope(submission.intent); | ||
var isSubmitting = submission.intent.slice(0, submission.intent.indexOf('/')) !== 'validate' && parseListCommand(submission.intent) === null; | ||
for (var element of form.elements) { | ||
if (isFieldElement(element) && element.willValidate) { | ||
var _submission$error$_el; | ||
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var messages = [].concat((_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : []); | ||
var shouldValidate = scope === null || scope === _elementName; | ||
if (shouldValidate) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) { | ||
element.focus(); | ||
focusedFirstInvalidField = true; | ||
} | ||
} | ||
} | ||
} | ||
function setValue(target, paths, valueFn) { | ||
var length = paths.length; | ||
var lastIndex = length - 1; | ||
var index = -1; | ||
var pointer = target; | ||
while (pointer != null && ++index < length) { | ||
var _pointer$key; | ||
var key = paths[index]; | ||
var next = paths[index + 1]; | ||
var newValue = index != lastIndex ? (_pointer$key = pointer[key]) !== null && _pointer$key !== void 0 ? _pointer$key : typeof next === 'number' ? [] : {} : valueFn(pointer[key]); | ||
pointer[key] = newValue; | ||
pointer = pointer[key]; | ||
} | ||
} | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
*/ | ||
function requestIntent(form, buttonProps) { | ||
if (!form) { | ||
console.warn('No form element is provided'); | ||
return; | ||
} | ||
var button = document.createElement('button'); | ||
button.name = INTENT; | ||
button.value = buttonProps.value; | ||
button.hidden = true; | ||
if (buttonProps.formNoValidate) { | ||
button.formNoValidate = true; | ||
} | ||
form.appendChild(button); | ||
button.click(); | ||
form.removeChild(button); | ||
} | ||
/** | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
* @see https://conform.guide/api/react#validate | ||
*/ | ||
function validate(field) { | ||
return { | ||
name: INTENT, | ||
value: field ? "validate/".concat(field) : 'validate', | ||
formNoValidate: true | ||
}; | ||
} | ||
function getFormElement(element) { | ||
var form = element instanceof HTMLFormElement ? element : element === null || element === void 0 ? void 0 : element.form; | ||
if (!form) { | ||
return null; | ||
} | ||
return form; | ||
} | ||
function parse(payload, options) { | ||
var submission = { | ||
intent: 'submit', | ||
payload: {}, | ||
error: {} | ||
}; | ||
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]; | ||
} | ||
}); | ||
} | ||
}; | ||
for (var [_name2, _value] of payload.entries()) { | ||
_loop(_value); | ||
} | ||
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(intent) { | ||
try { | ||
var [group, type, scope, json] = intent.split('/'); | ||
if (group !== 'list' || !['prepend', 'append', 'replace', 'remove', 'reorder'].includes(type) || !scope) { | ||
return null; | ||
} | ||
var _payload = JSON.parse(json); | ||
return { | ||
// @ts-expect-error | ||
type, | ||
scope, | ||
payload: _payload | ||
}; | ||
} catch (error) { | ||
return null; | ||
} | ||
} | ||
function updateList(list, command) { | ||
switch (command.type) { | ||
case 'prepend': | ||
{ | ||
list.unshift(command.payload.defaultValue); | ||
break; | ||
} | ||
case 'append': | ||
{ | ||
list.push(command.payload.defaultValue); | ||
break; | ||
} | ||
case 'replace': | ||
{ | ||
list.splice(command.payload.index, 1, command.payload.defaultValue); | ||
break; | ||
} | ||
case 'remove': | ||
list.splice(command.payload.index, 1); | ||
break; | ||
case 'reorder': | ||
list.splice(command.payload.to, 0, ...list.splice(command.payload.from, 1)); | ||
break; | ||
default: | ||
throw new Error('Unknown list command received'); | ||
} | ||
return list; | ||
} | ||
/** | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
* @see https://conform.guide/api/react#list | ||
*/ | ||
var list = new Proxy({}, { | ||
get(_target, type) { | ||
switch (type) { | ||
case 'append': | ||
case 'prepend': | ||
case 'replace': | ||
case 'remove': | ||
case 'reorder': | ||
return function (scope) { | ||
var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
return { | ||
name: INTENT, | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
formNoValidate: true | ||
}; | ||
}; | ||
} | ||
} | ||
}); | ||
/** | ||
* 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_ERROR_ELEMENT_NAME ? 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 { FORM_ERROR_ELEMENT_NAME, INTENT, VALIDATION_SKIPPED, VALIDATION_UNDEFINED, getErrors, getFormAttributes, getFormData, getFormElement, getName, getPaths, getScope, getValidationMessage, isFieldElement, isFocusedOnIntentButton, list, parse, parseListCommand, reportSubmission, requestIntent, setValue, updateList, validate, validateConstraint }; | ||
export { createSubmitter, focusFirstInvalidControl, focusFormControl, getFormAction, getFormControls, getFormElement, getFormEncType, getFormMethod, isFormControl as isFieldElement, isFocusableFormControl, requestSubmit } from './dom.js'; | ||
export { getErrors, getFormData, formatPaths as getName, getPaths, getValidationMessage } from './formdata.js'; | ||
export { INTENT, getScope, isSubmitting, list, parseListCommand, requestIntent, updateList, validate } from './intent.js'; | ||
export { parse } from './parse.js'; |
@@ -6,3 +6,3 @@ { | ||
"license": "MIT", | ||
"version": "0.6.1", | ||
"version": "0.6.2", | ||
"main": "index.js", | ||
@@ -9,0 +9,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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
21
1200
41982
1
1