From: GeoSot Date: Wed, 19 May 2021 15:37:26 +0000 (+0300) Subject: Make a form validation handler | handle form messages X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=aa6a1ece56c1e9109980934da12ac696ae7dba36;p=thirdparty%2Fbootstrap.git Make a form validation handler | handle form messages add "aria-describedby" attribute on "supported elements" section add "aria-describedby" attribute on server side succeed validation messages --- diff --git a/js/index.esm.js b/js/index.esm.js index 062b25408f..1b3806c5ed 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -10,6 +10,7 @@ export { default as Button } from './src/button' export { default as Carousel } from './src/carousel' export { default as Collapse } from './src/collapse' export { default as Dropdown } from './src/dropdown' +export { default as Form } from './src/forms/form' export { default as Modal } from './src/modal' export { default as Offcanvas } from './src/offcanvas' export { default as Popover } from './src/popover' diff --git a/js/index.umd.js b/js/index.umd.js index c63d7c2079..8e054aabae 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -9,6 +9,7 @@ import Alert from './src/alert' import Button from './src/button' import Carousel from './src/carousel' import Collapse from './src/collapse' +import Form from './src/forms/form' import Dropdown from './src/dropdown' import Modal from './src/modal' import Offcanvas from './src/offcanvas' @@ -23,6 +24,7 @@ export default { Button, Carousel, Collapse, + Form, Dropdown, Modal, Offcanvas, diff --git a/js/src/forms/form-field.js b/js/src/forms/form-field.js new file mode 100644 index 0000000000..e8fd5223c5 --- /dev/null +++ b/js/src/forms/form-field.js @@ -0,0 +1,127 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.3.0): forms/field.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { getUID, isElement } from '../util/index' +import EventHandler from '../dom/event-handler' +import BaseComponent from '../base-component' +import SelectorEngine from '../dom/selector-engine' +import TemplateFactory from '../util/template-factory' + +const NAME = 'formField' +const DATA_KEY = 'bs.field' +const EVENT_KEY = `.${DATA_KEY}` +const EVENT_INPUT = `input${EVENT_KEY}` +const CLASS_FIELD_ERROR = 'is-invalid' +const CLASS_FIELD_SUCCESS = 'is-valid' + +const ARIA_DESCRIBED_BY = 'aria-describedby' +const Default = { + invalid: '', // invalid message to add + name: null, + valid: '', // valid message to add + type: 'feedback' // or tooltip +} + +const DefaultType = { + invalid: 'string', + name: 'string', + valid: 'string', + type: 'string' +} + +const MessageTypes = { + ERROR: { prefix: 'invalid', class: CLASS_FIELD_ERROR }, + INFO: { prefix: 'info', class: '' }, + SUCCESS: { prefix: 'valid', class: CLASS_FIELD_SUCCESS } +} + +class FormField extends BaseComponent { + constructor(element, config) { + super(element, config) + if (!isElement(this._element)) { + throw new TypeError(`field "${this._config.name}" not found`) + } + + this._tipId = getUID(`${this._config.name}-formTip-`) + this._initialDescribedBy = this._element.getAttribute(ARIA_DESCRIBED_BY) || '' + + EventHandler.on(this._element, EVENT_INPUT, () => { + this.clearAppended() + }) + } + + static get NAME() { + return NAME + } + + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get MessageTypes() { + return MessageTypes + } + + getElement() { + return this._element + } + + clearAppended() { + const appendedFeedback = SelectorEngine.findOne(`#${this._tipId}`, this._element.parentNode) + if (!appendedFeedback) { + return + } + + appendedFeedback.remove() + + this._element.classList.remove(CLASS_FIELD_ERROR, CLASS_FIELD_SUCCESS) + + if (this._initialDescribedBy) { + this._element.setAttribute(ARIA_DESCRIBED_BY, this._initialDescribedBy) + return + } + + this._element.removeAttribute(ARIA_DESCRIBED_BY) + } + + appendError(message = this._config.invalid) { + return this.appendFeedback(message, this.constructor.MessageTypes.ERROR) + } + + appendSuccess(message = this._config.valid) { + return this.appendFeedback(message, this.constructor.MessageTypes.SUCCESS) + } + + appendFeedback(feedback, classes = this.constructor.MessageTypes.INFO) { + if (!feedback) { + return false + } + + this.clearAppended() + + const config = { + extraClass: `${classes.prefix}-${this._config.type} ${classes.class}`, + content: { div: feedback } + } + feedback = new TemplateFactory(config) + + const feedbackElement = feedback.toHtml() + feedbackElement.id = this._tipId + + this._element.parentNode.append(feedbackElement) + + const describedBy = `${this._initialDescribedBy} ${feedbackElement.id}`.trim() + this._element.setAttribute(ARIA_DESCRIBED_BY, describedBy) + return true + } +} + +export default FormField diff --git a/js/src/forms/form.js b/js/src/forms/form.js new file mode 100644 index 0000000000..da47037890 --- /dev/null +++ b/js/src/forms/form.js @@ -0,0 +1,161 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.3.0): util/form-validation.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ +import BaseComponent from '../base-component' +import EventHandler from '../dom/event-handler' +import FormField from './form-field' +import SelectorEngine from '../dom/selector-engine' + +const NAME = 'formValidation' +const DATA_KEY = 'bs.formValidation' +const EVENT_KEY = `.${DATA_KEY}` +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}` +const EVENT_SUBMIT = `submit${EVENT_KEY}` +const EVENT_RESET = `reset${EVENT_KEY}` + +const CLASS_VALIDATED = 'was-validated' +const SELECTOR_DATA_TOGGLE = 'form[data-bs-toggle="form-validation"]' + +const Default = { + type: 'feedback', // or 'tooltip' + validateCallback: null +} + +const DefaultType = { + type: 'string', validateCallback: '(function|null)' +} + +class Form extends BaseComponent { + constructor(element, config) { + if (element.tagName !== 'FORM') { + throw new TypeError(`Need to be initialized in form elements. "${element.tagName}" given`) + } + + super(element, config) + + this._formFields = null // form field instances + } + + static get NAME() { + return NAME + } + + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + getFields() { + if (!this._formFields) { + this._formFields = this._initializeFields() + } + + return this._formFields + } + + getField(name) { + return this.getFields().get(name) + } + + clear() { + this._element.classList.remove(CLASS_VALIDATED) + // eslint-disable-next-line no-unused-vars + for (const [name, field] of this.getFields()) { + field.clearAppended() + } + } + + validate() { + this.clear() + const fetchedErrors = this._fetchErrors() + if (this._element.checkValidity() && !Object.keys(fetchedErrors).length) { + return true + } + + for (const [name, field] of this.getFields()) { + this._appendErrorToField(field, fetchedErrors[name] || null) + } + + this._element.classList.add(CLASS_VALIDATED) + return false + } + + getDataForSubmission() { + return new FormData(this._element) + } + + _appendErrorToField(field, givenMessage) { + const element = field.getElement() + + if (givenMessage) { // if field is invalid check and return for default message + field.appendError(givenMessage) + return + } + + if (element.checkValidity()) { // if field is valid, return first success message + field.appendSuccess() + return + } + + if (field.appendError()) { // if field is invalid check and return for default message + return + } + + field.appendError(element.validationMessage) + } + + _initializeFields() { + const fields = new Map() + const formElements = Array.from(this._element.elements) // the DOM elements + for (const element of formElements) { + const name = element.name || element.id + + const field = FormField.getOrCreateInstance(element, { + name, type: this._config.type + }) + fields.set(name, field) + } + + return fields + } + + _fetchErrors() { + return typeof this._config.validateCallback === 'function' ? this._config.validateCallback(this.getDataForSubmission()) : {} + } +} + +// On submit we want to auto-validate form +EventHandler.on(document, EVENT_SUBMIT, SELECTOR_DATA_TOGGLE, event => { + const { target } = event + const instance = Form.getOrCreateInstance(target) + if (!target.checkValidity()) { + event.preventDefault() + event.stopPropagation() + } + + if (instance.validate()) { + target.submit() + } +}) + +EventHandler.on(document, EVENT_RESET, SELECTOR_DATA_TOGGLE, event => { + const { target } = event + const instance = Form.getOrCreateInstance(target) + + instance.clear() +}) + +// On load, add `novalidate` attribute to avoid browser validation +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const el of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) { + el.setAttribute('novalidate', true) + } +}) +export default Form + diff --git a/site/content/docs/5.2/examples/checkout-rtl/index.html b/site/content/docs/5.2/examples/checkout-rtl/index.html index e2a7971c1a..44120b272b 100644 --- a/site/content/docs/5.2/examples/checkout-rtl/index.html +++ b/site/content/docs/5.2/examples/checkout-rtl/index.html @@ -4,8 +4,6 @@ title: مثال إتمام الشراء direction: rtl extra_css: - "../checkout/checkout.css" -extra_js: - - src: "../checkout/checkout.js" body_class: "bg-light" --- @@ -67,7 +65,7 @@ body_class: "bg-light"

عنوان الفوترة

-
+
diff --git a/site/content/docs/5.2/examples/checkout/checkout.js b/site/content/docs/5.2/examples/checkout/checkout.js index 30ea0aa6b1..e69de29bb2 100644 --- a/site/content/docs/5.2/examples/checkout/checkout.js +++ b/site/content/docs/5.2/examples/checkout/checkout.js @@ -1,19 +0,0 @@ -// Example starter JavaScript for disabling form submissions if there are invalid fields -(() => { - 'use strict' - - // Fetch all the forms we want to apply custom Bootstrap validation styles to - const forms = document.querySelectorAll('.needs-validation') - - // Loop over them and prevent submission - Array.from(forms).forEach(form => { - form.addEventListener('submit', event => { - if (!form.checkValidity()) { - event.preventDefault() - event.stopPropagation() - } - - form.classList.add('was-validated') - }, false) - }) -})() diff --git a/site/content/docs/5.2/examples/checkout/index.html b/site/content/docs/5.2/examples/checkout/index.html index ba415f0d9f..87b03634bd 100644 --- a/site/content/docs/5.2/examples/checkout/index.html +++ b/site/content/docs/5.2/examples/checkout/index.html @@ -3,8 +3,6 @@ layout: examples title: Checkout example extra_css: - "checkout.css" -extra_js: - - src: "checkout.js" body_class: "bg-light" --- @@ -66,7 +64,7 @@ body_class: "bg-light"

Billing address

- +
diff --git a/site/content/docs/5.2/forms/validation.md b/site/content/docs/5.2/forms/validation.md index f8c2200c00..aa789676ec 100644 --- a/site/content/docs/5.2/forms/validation.md +++ b/site/content/docs/5.2/forms/validation.md @@ -4,9 +4,6 @@ title: Validation description: Provide valuable, actionable feedback to your users with HTML5 form validation, via browser default behaviors or custom styles and JavaScript. group: forms toc: true -extra_js: - - src: "/docs/5.2/assets/js/validate-forms.js" - async: true --- {{< callout warning >}} @@ -30,80 +27,59 @@ With that in mind, consider the following demos for our custom form validation s ## Custom styles -For custom Bootstrap form validation messages, you'll need to add the `novalidate` boolean attribute to your ``. This disables the browser default feedback tooltips, but still provides access to the form validation APIs in JavaScript. Try to submit the form below; our JavaScript will intercept the submit button and relay feedback to you. When attempting to submit, you'll see the `:invalid` and `:valid` styles applied to your form controls. +For custom Bootstrap form validation messages, you'll need to add the data-bs-toggle="form-validation" ``. This disables the browser default feedback tooltips, but still provides access to the form validation APIs in JavaScript. Try to submit the form below; our JavaScript will intercept the submit button and relay feedback to you. When attempting to submit, you'll see the `:invalid` and `:valid` styles applied to your form controls. Custom feedback styles apply custom colors, borders, focus styles, and background icons to better communicate feedback. Background icons for ` -
- Looks good! -
+
- -
- Looks good! -
+
@ - -
- Please choose a username. -
+
- -
- Please provide a valid city. -
+
- -
- Please select a valid state. -
- -
- Please provide a valid zip. -
+
- + -
- You must agree before submitting. -
+
{{< /example >}} {{< example lang="js" show_preview="false" >}} {{< js.inline >}} -{{- readFile (path.Join "site/static/docs" .Site.Params.docs_version "assets/js/validate-forms.js") -}} {{< /js.inline >}} {{< /example >}} @@ -171,15 +147,15 @@ To fix [issues with border radius](https://github.com/twbs/bootstrap/issues/2511
- -
+ +
Looks good!
- -
+ +
Looks good!
@@ -246,41 +222,41 @@ Validation styles are available for the following form controls and components:
- -
+ +
Please enter a message in the textarea.
- + -
Example invalid feedback text
+
Example invalid feedback text
- +
- + -
More example invalid feedback text
+
More example invalid feedback text
- -
Example invalid select feedback
+
Example invalid select feedback
- -
Example invalid form file feedback
+ +
Example invalid form file feedback
@@ -294,57 +270,40 @@ Validation styles are available for the following form controls and components: If your form layout allows it, you can swap the `.{valid|invalid}-feedback` classes for `.{valid|invalid}-tooltip` classes to display validation feedback in a styled tooltip. Be sure to have a parent with `position: relative` on it for tooltip positioning. In the example below, our column classes have this already, but your project may require an alternative setup. {{< example >}} - +
- -
- Looks good! -
+
- -
- Looks good! -
+
@ - -
- Please choose a unique and valid username. -
+
- -
- Please provide a valid city. -
+
- -
- Please select a valid state. -
- -
- Please provide a valid zip. -
+
+
{{< /example >}} diff --git a/site/static/docs/5.2/assets/js/validate-forms.js b/site/static/docs/5.2/assets/js/validate-forms.js index 30ea0aa6b1..e69de29bb2 100644 --- a/site/static/docs/5.2/assets/js/validate-forms.js +++ b/site/static/docs/5.2/assets/js/validate-forms.js @@ -1,19 +0,0 @@ -// Example starter JavaScript for disabling form submissions if there are invalid fields -(() => { - 'use strict' - - // Fetch all the forms we want to apply custom Bootstrap validation styles to - const forms = document.querySelectorAll('.needs-validation') - - // Loop over them and prevent submission - Array.from(forms).forEach(form => { - form.addEventListener('submit', event => { - if (!form.checkValidity()) { - event.preventDefault() - event.stopPropagation() - } - - form.classList.add('was-validated') - }, false) - }) -})()