@conform-to/dom
Advanced tools
Comparing version 0.6.0-pre.0 to 0.6.0
@@ -10,2 +10,10 @@ export type Primitive = null | undefined | string | number | boolean | Date; | ||
errorId?: string; | ||
/** | ||
* The frist error of the field | ||
*/ | ||
error?: string; | ||
/** | ||
* All of the field errors | ||
*/ | ||
errors?: string[]; | ||
} | ||
@@ -40,8 +48,15 @@ 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> ? { | ||
export interface IntentButtonProps { | ||
name: '__intent__'; | ||
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; | ||
export declare function getFormElements(form: HTMLFormElement): 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>; | ||
@@ -57,5 +72,8 @@ export declare function getFormData(form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null): FormData; | ||
export declare function getName(paths: Array<string | number>): string; | ||
export declare function shouldValidate(intent: string, name: string): boolean; | ||
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__"; | ||
@@ -66,8 +84,2 @@ export declare const VALIDATION_SKIPPED = "__skipped__"; | ||
/** | ||
* The ponyfill of `HTMLFormElement.requestSubmit()` | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit | ||
* @see https://caniuse.com/?search=requestSubmit | ||
*/ | ||
export declare function requestSubmit(form: HTMLFormElement, submitter?: HTMLButtonElement | HTMLInputElement): void; | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
@@ -80,3 +92,3 @@ */ | ||
/** | ||
* Returns the properties required to configure a command button for validation | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
@@ -87,3 +99,2 @@ * @see https://conform.guide/api/react#validate | ||
export declare function getFormElement(element: HTMLFormElement | HTMLFieldSetElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | null): HTMLFormElement | null; | ||
export declare function focus(field: FieldElement): void; | ||
export declare function parse(payload: FormData | URLSearchParams): Submission; | ||
@@ -170,3 +181,3 @@ export declare function parse<Schema>(payload: FormData | URLSearchParams, options?: { | ||
/** | ||
* Helpers to configure a command button for modifying a list | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
@@ -173,0 +184,0 @@ * @see https://conform.guide/api/react#list |
111
index.js
@@ -7,23 +7,14 @@ '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>> | ''; | ||
/** | ||
* 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'); | ||
} | ||
function getFormElements(form) { | ||
return Array.from(form.elements).filter(isFieldElement); | ||
} | ||
/** | ||
* Find the corresponding paths based on the formatted name | ||
* @param name formatted name | ||
* @returns paths | ||
*/ | ||
function getPaths(name) { | ||
@@ -75,14 +66,18 @@ var pattern = /(\w*)\[(\d+)\]/; | ||
} | ||
function shouldValidate(intent, name) { | ||
var _parseListCommand; | ||
var [type] = intent.split('/', 1); | ||
function getScope(intent) { | ||
var _parseListCommand$sco, _parseListCommand; | ||
var [type, ...rest] = intent.split('/'); | ||
switch (type) { | ||
case 'validate': | ||
return intent === 'validate' || intent === "validate/".concat(name); | ||
return rest.length > 0 ? rest.join('/') : null; | ||
case 'list': | ||
return ((_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) === name; | ||
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null; | ||
default: | ||
return true; | ||
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) { | ||
@@ -97,2 +92,4 @@ return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31)); | ||
} | ||
var FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
var INTENT = '__intent__'; | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
@@ -109,3 +106,3 @@ var VALIDATION_SKIPPED = '__skipped__'; | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var elementName = _name ? _name : FORM_ERROR_ELEMENT_NAME; | ||
var item = form.elements.namedItem(elementName); | ||
@@ -129,19 +126,24 @@ if (item instanceof RadioNodeList) { | ||
} | ||
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 _elementName = element.name !== '__form__' ? element.name : ''; | ||
var _message = submission.error[_elementName]; | ||
var elementShouldValidate = shouldValidate(submission.intent, _elementName); | ||
if (elementShouldValidate) { | ||
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 (typeof _message === 'undefined' || ![].concat(_message).includes(VALIDATION_SKIPPED)) { | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(getValidationMessage(_message)); | ||
element.setCustomValidity(getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
if (elementShouldValidate && !element.validity.valid) { | ||
focus(element); | ||
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) { | ||
element.focus(); | ||
focusedFirstInvalidField = true; | ||
} | ||
@@ -167,16 +169,2 @@ } | ||
/** | ||
* The ponyfill of `HTMLFormElement.requestSubmit()` | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit | ||
* @see https://caniuse.com/?search=requestSubmit | ||
*/ | ||
function requestSubmit(form, submitter) { | ||
var submitEvent = new SubmitEvent('submit', { | ||
bubbles: true, | ||
cancelable: true, | ||
submitter | ||
}); | ||
form.dispatchEvent(submitEvent); | ||
} | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
@@ -190,3 +178,3 @@ */ | ||
var button = document.createElement('button'); | ||
button.name = '__intent__'; | ||
button.name = INTENT; | ||
button.value = buttonProps.value; | ||
@@ -203,3 +191,3 @@ button.hidden = true; | ||
/** | ||
* Returns the properties required to configure a command button for validation | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
@@ -210,3 +198,3 @@ * @see https://conform.guide/api/react#validate | ||
return { | ||
name: '__intent__', | ||
name: INTENT, | ||
value: field ? "validate/".concat(field) : 'validate', | ||
@@ -223,9 +211,2 @@ formNoValidate: true | ||
} | ||
function focus(field) { | ||
var currentFocus = document.activeElement; | ||
if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== field.form) { | ||
return; | ||
} | ||
field.focus(); | ||
} | ||
function parse(payload, options) { | ||
@@ -238,3 +219,3 @@ var submission = { | ||
var _loop = function _loop(_value) { | ||
if (_name2 === '__intent__') { | ||
if (_name2 === INTENT) { | ||
if (typeof _value !== 'string' || submission.intent !== 'submit') { | ||
@@ -337,3 +318,3 @@ throw new Error('The intent could only be set on a button'); | ||
/** | ||
* Helpers to configure a command button for modifying a list | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
@@ -353,3 +334,3 @@ * @see https://conform.guide/api/react#list | ||
return { | ||
name: '__intent__', | ||
name: INTENT, | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
@@ -400,3 +381,3 @@ formNoValidate: true | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var _name3 = element.name === '__form__' ? '' : element.name; | ||
var _name3 = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => { | ||
@@ -445,5 +426,6 @@ var [name, attributeValue = ''] = _ref4; | ||
exports.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME; | ||
exports.INTENT = INTENT; | ||
exports.VALIDATION_SKIPPED = VALIDATION_SKIPPED; | ||
exports.VALIDATION_UNDEFINED = VALIDATION_UNDEFINED; | ||
exports.focus = focus; | ||
exports.getErrors = getErrors; | ||
@@ -453,7 +435,8 @@ exports.getFormAttributes = getFormAttributes; | ||
exports.getFormElement = getFormElement; | ||
exports.getFormElements = getFormElements; | ||
exports.getName = getName; | ||
exports.getPaths = getPaths; | ||
exports.getScope = getScope; | ||
exports.getValidationMessage = getValidationMessage; | ||
exports.isFieldElement = isFieldElement; | ||
exports.isFocusedOnIntentButton = isFocusedOnIntentButton; | ||
exports.list = list; | ||
@@ -464,7 +447,5 @@ exports.parse = parse; | ||
exports.requestIntent = requestIntent; | ||
exports.requestSubmit = requestSubmit; | ||
exports.setValue = setValue; | ||
exports.shouldValidate = shouldValidate; | ||
exports.updateList = updateList; | ||
exports.validate = validate; | ||
exports.validateConstraint = validateConstraint; |
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>> | ''; | ||
/** | ||
* 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'); | ||
} | ||
function getFormElements(form) { | ||
return Array.from(form.elements).filter(isFieldElement); | ||
} | ||
/** | ||
* Find the corresponding paths based on the formatted name | ||
* @param name formatted name | ||
* @returns paths | ||
*/ | ||
function getPaths(name) { | ||
@@ -70,14 +61,18 @@ var pattern = /(\w*)\[(\d+)\]/; | ||
} | ||
function shouldValidate(intent, name) { | ||
var _parseListCommand; | ||
var [type] = intent.split('/', 1); | ||
function getScope(intent) { | ||
var _parseListCommand$sco, _parseListCommand; | ||
var [type, ...rest] = intent.split('/'); | ||
switch (type) { | ||
case 'validate': | ||
return intent === 'validate' || intent === "validate/".concat(name); | ||
return rest.length > 0 ? rest.join('/') : null; | ||
case 'list': | ||
return ((_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) === name; | ||
return (_parseListCommand$sco = (_parseListCommand = parseListCommand(intent)) === null || _parseListCommand === void 0 ? void 0 : _parseListCommand.scope) !== null && _parseListCommand$sco !== void 0 ? _parseListCommand$sco : null; | ||
default: | ||
return true; | ||
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) { | ||
@@ -92,2 +87,4 @@ return [].concat(errors !== null && errors !== void 0 ? errors : []).join(String.fromCharCode(31)); | ||
} | ||
var FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
var INTENT = '__intent__'; | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
@@ -104,3 +101,3 @@ var VALIDATION_SKIPPED = '__skipped__'; | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = _name ? _name : '__form__'; | ||
var elementName = _name ? _name : FORM_ERROR_ELEMENT_NAME; | ||
var item = form.elements.namedItem(elementName); | ||
@@ -124,19 +121,24 @@ if (item instanceof RadioNodeList) { | ||
} | ||
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 _elementName = element.name !== '__form__' ? element.name : ''; | ||
var _message = submission.error[_elementName]; | ||
var elementShouldValidate = shouldValidate(submission.intent, _elementName); | ||
if (elementShouldValidate) { | ||
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 (typeof _message === 'undefined' || ![].concat(_message).includes(VALIDATION_SKIPPED)) { | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(getValidationMessage(_message)); | ||
element.setCustomValidity(getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
if (elementShouldValidate && !element.validity.valid) { | ||
focus(element); | ||
if (!focusedFirstInvalidField && (isSubmitting || isFocusedOnIntentButton(form, submission.intent)) && shouldValidate && element.tagName !== 'BUTTON' && !element.validity.valid) { | ||
element.focus(); | ||
focusedFirstInvalidField = true; | ||
} | ||
@@ -162,16 +164,2 @@ } | ||
/** | ||
* The ponyfill of `HTMLFormElement.requestSubmit()` | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit | ||
* @see https://caniuse.com/?search=requestSubmit | ||
*/ | ||
function requestSubmit(form, submitter) { | ||
var submitEvent = new SubmitEvent('submit', { | ||
bubbles: true, | ||
cancelable: true, | ||
submitter | ||
}); | ||
form.dispatchEvent(submitEvent); | ||
} | ||
/** | ||
* Creates an intent button on demand and trigger a form submit by clicking it. | ||
@@ -185,3 +173,3 @@ */ | ||
var button = document.createElement('button'); | ||
button.name = '__intent__'; | ||
button.name = INTENT; | ||
button.value = buttonProps.value; | ||
@@ -198,3 +186,3 @@ button.hidden = true; | ||
/** | ||
* Returns the properties required to configure a command button for validation | ||
* Returns the properties required to configure an intent button for validation | ||
* | ||
@@ -205,3 +193,3 @@ * @see https://conform.guide/api/react#validate | ||
return { | ||
name: '__intent__', | ||
name: INTENT, | ||
value: field ? "validate/".concat(field) : 'validate', | ||
@@ -218,9 +206,2 @@ formNoValidate: true | ||
} | ||
function focus(field) { | ||
var currentFocus = document.activeElement; | ||
if (!isFieldElement(currentFocus) || currentFocus.tagName !== 'BUTTON' || currentFocus.form !== field.form) { | ||
return; | ||
} | ||
field.focus(); | ||
} | ||
function parse(payload, options) { | ||
@@ -233,3 +214,3 @@ var submission = { | ||
var _loop = function _loop(_value) { | ||
if (_name2 === '__intent__') { | ||
if (_name2 === INTENT) { | ||
if (typeof _value !== 'string' || submission.intent !== 'submit') { | ||
@@ -332,3 +313,3 @@ throw new Error('The intent could only be set on a button'); | ||
/** | ||
* Helpers to configure a command button for modifying a list | ||
* Helpers to configure an intent button for modifying a list | ||
* | ||
@@ -348,3 +329,3 @@ * @see https://conform.guide/api/react#list | ||
return { | ||
name: '__intent__', | ||
name: INTENT, | ||
value: "list/".concat(type, "/").concat(scope, "/").concat(JSON.stringify(payload)), | ||
@@ -395,3 +376,3 @@ formNoValidate: true | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var _name3 = element.name === '__form__' ? '' : element.name; | ||
var _name3 = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref4) => { | ||
@@ -440,2 +421,2 @@ var [name, attributeValue = ''] = _ref4; | ||
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 }; | ||
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 }; |
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "0.6.0-pre.0", | ||
"version": "0.6.0", | ||
"main": "index.js", | ||
@@ -8,0 +8,0 @@ "module": "module/index.js", |
@@ -5,8 +5,14 @@ # @conform-to/dom | ||
Conform is a form validation library built on top of the [Constraint Validation](https://caniuse.com/constraint-validation) API. | ||
Conform is a progressive enhancement first form validation library for [Remix](https://remix.run) | ||
- **Progressive Enhancement**: It is designed based on the [HTML specification](https://html.spec.whatwg.org/dev/form-control-infrastructure.html#the-constraint-validation-api). From validating the form to reporting error messages for each field, if you don't like part of the solution, just replace it with your own. | ||
- **Framework Agnostic**: The DOM is the only dependency. Conform makes use of native [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) exclusively. You don't have to use React / Vue / Svelte to utilise this library. | ||
- **Flexible Setup**: It can validates fields anywhere in the dom with the help of [form attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form). Also enables CSS pseudo-classes like `:valid` and `:invalid`, allowing flexible styling across your form without the need to manipulate the class names. | ||
### Highlights | ||
- Focused on progressive enhancment by default | ||
- Simplifed intergration through event delegation | ||
- Server first validation with Zod / Yup schema support | ||
- Field name inference with type checking | ||
- Focus management | ||
- Accessibility support | ||
- About 5kb compressed | ||
Checkout the [repository](https://github.com/edmundhung/conform) if you want to know more! |
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
18
44503
1136