mirror of
https://github.com/twbs/bootstrap.git
synced 2025-08-27 15:19:52 +02:00
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
This commit is contained in:
@@ -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'
|
||||
|
@@ -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,
|
||||
|
127
js/src/forms/form-field.js
Normal file
127
js/src/forms/form-field.js
Normal file
@@ -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
|
161
js/src/forms/form.js
Normal file
161
js/src/forms/form.js
Normal file
@@ -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
|
||||
|
Reference in New Issue
Block a user