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'
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'
Button,
Carousel,
Collapse,
+ Form,
Dropdown,
Modal,
Offcanvas,
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * 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
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * 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
+
direction: rtl
extra_css:
- "../checkout/checkout.css"
-extra_js:
- - src: "../checkout/checkout.js"
body_class: "bg-light"
---
</div>
<div class="col-md-7 col-lg-8">
<h4 class="mb-3">عنوان الفوترة</h4>
- <form class="needs-validation" novalidate>
+ <form data-bs-toggle="form-validation">
<div class="row g-3">
<div class="col-sm-6">
<label for="firstName" class="form-label">الاسم الأول</label>
-// 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)
- })
-})()
title: Checkout example
extra_css:
- "checkout.css"
-extra_js:
- - src: "checkout.js"
body_class: "bg-light"
---
</div>
<div class="col-md-7 col-lg-8">
<h4 class="mb-3">Billing address</h4>
- <form class="needs-validation" novalidate>
+ <form data-bs-toggle="form-validation">
<div class="row g-3">
<div class="col-sm-6">
<label for="firstName" class="form-label">First name</label>
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 >}}
## Custom styles
-For custom Bootstrap form validation messages, you'll need to add the `novalidate` boolean attribute to your `<form>`. 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" `<form>`. 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 `<select>`s are only available with `.form-select`, and not `.form-control`.
{{< example >}}
-<form class="row g-3 needs-validation" novalidate>
+<form class="row g-3" data-bs-toggle="form-validation">
<div class="col-md-4">
<label for="validationCustom01" class="form-label">First name</label>
- <input type="text" class="form-control" id="validationCustom01" value="Mark" required>
- <div class="valid-feedback">
- Looks good!
- </div>
+ <input type="text" class="form-control" id="validationCustom01" value="Mark" required data-bs-valid="Looks good!" data-bs-invalid="Please, provide a valid Name!">
</div>
<div class="col-md-4">
<label for="validationCustom02" class="form-label">Last name</label>
- <input type="text" class="form-control" id="validationCustom02" value="Otto" required>
- <div class="valid-feedback">
- Looks good!
- </div>
+ <input type="text" class="form-control" id="validationCustom02" value="Otto" required data-bs-valid="Looks good!">
</div>
<div class="col-md-4">
<label for="validationCustomUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="inputGroupPrepend">@</span>
- <input type="text" class="form-control" id="validationCustomUsername" aria-describedby="inputGroupPrepend" required>
- <div class="invalid-feedback">
- Please choose a username.
- </div>
+ <input type="text" class="form-control" id="validationCustomUsername" aria-describedby="inputGroupPrepend" required data-bs-invalid="Please choose a username.">
</div>
</div>
<div class="col-md-6">
<label for="validationCustom03" class="form-label">City</label>
- <input type="text" class="form-control" id="validationCustom03" required>
- <div class="invalid-feedback">
- Please provide a valid city.
- </div>
+ <input type="text" class="form-control" id="validationCustom03" required data-bs-invalid="Please provide a valid city.">
</div>
<div class="col-md-3">
<label for="validationCustom04" class="form-label">State</label>
- <select class="form-select" id="validationCustom04" required>
+ <select class="form-select" id="validationCustom04" required data-bs-invalid="Please select a valid state.">
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
- <div class="invalid-feedback">
- Please select a valid state.
- </div>
</div>
<div class="col-md-3">
<label for="validationCustom05" class="form-label">Zip</label>
- <input type="text" class="form-control" id="validationCustom05" required>
- <div class="invalid-feedback">
- Please provide a valid zip.
- </div>
+ <input type="text" class="form-control" id="validationCustom05" required data-bs-invalid="Please provide a valid zip.">
</div>
<div class="col-12">
<div class="form-check">
- <input class="form-check-input" type="checkbox" value="" id="invalidCheck" required>
+ <input class="form-check-input" type="checkbox" value="" id="invalidCheck" required data-bs-invalid="You must agree before submitting.">
<label class="form-check-label" for="invalidCheck">
Agree to terms and conditions
</label>
- <div class="invalid-feedback">
- You must agree before submitting.
- </div>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
+ <button class="btn btn-danger" type="reset">Reset form</button>
</div>
</form>
{{< /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 >}}
<form class="row g-3">
<div class="col-md-4">
<label for="validationServer01" class="form-label">First name</label>
- <input type="text" class="form-control is-valid" id="validationServer01" value="Mark" required>
- <div class="valid-feedback">
+ <input type="text" class="form-control is-valid" id="validationServer01" value="Mark" aria-describedby="validationServerNameFeedback" required>
+ <div id="validationServerNameFeedback" class="valid-feedback">
Looks good!
</div>
</div>
<div class="col-md-4">
<label for="validationServer02" class="form-label">Last name</label>
- <input type="text" class="form-control is-valid" id="validationServer02" value="Otto" required>
- <div class="valid-feedback">
+ <input type="text" class="form-control is-valid" id="validationServer02" aria-describedby="validationServerLastNameFeedback" value="Otto" required>
+ <div id="validationServerLastNameFeedback" class="valid-feedback">
Looks good!
</div>
</div>
<form class="was-validated">
<div class="mb-3">
<label for="validationTextarea" class="form-label">Textarea</label>
- <textarea class="form-control" id="validationTextarea" placeholder="Required example textarea" required></textarea>
- <div class="invalid-feedback">
+ <textarea class="form-control" id="validationTextarea" placeholder="Required example textarea" aria-describedby="validationSupportedElementsTextArea" required></textarea>
+ <div id="validationSupportedElementsTextArea" class="invalid-feedback">
Please enter a message in the textarea.
</div>
</div>
<div class="form-check mb-3">
- <input type="checkbox" class="form-check-input" id="validationFormCheck1" required>
+ <input type="checkbox" class="form-check-input" id="validationFormCheck1" aria-describedby="validationSupportedElementsCheckBox" required>
<label class="form-check-label" for="validationFormCheck1">Check this checkbox</label>
- <div class="invalid-feedback">Example invalid feedback text</div>
+ <div id="validationSupportedElementsCheckBox" class="invalid-feedback">Example invalid feedback text</div>
</div>
<div class="form-check">
- <input type="radio" class="form-check-input" id="validationFormCheck2" name="radio-stacked" required>
+ <input type="radio" class="form-check-input" id="validationFormCheck2" name="radio-stacked" aria-describedby="validationSupportedElementsRadio" required>
<label class="form-check-label" for="validationFormCheck2">Toggle this radio</label>
</div>
<div class="form-check mb-3">
- <input type="radio" class="form-check-input" id="validationFormCheck3" name="radio-stacked" required>
+ <input type="radio" class="form-check-input" id="validationFormCheck3" name="radio-stacked" aria-describedby="validationSupportedElementsRadio" required>
<label class="form-check-label" for="validationFormCheck3">Or toggle this other radio</label>
- <div class="invalid-feedback">More example invalid feedback text</div>
+ <div id="validationSupportedElementsRadio" class="invalid-feedback">More example invalid feedback text</div>
</div>
<div class="mb-3">
- <select class="form-select" required aria-label="select example">
+ <select class="form-select" required aria-label="select example" aria-describedby="validationSupportedElementsSelect">
<option value="">Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
- <div class="invalid-feedback">Example invalid select feedback</div>
+ <div id="validationSupportedElementsSelect" class="invalid-feedback">Example invalid select feedback</div>
</div>
<div class="mb-3">
- <input type="file" class="form-control" aria-label="file example" required>
- <div class="invalid-feedback">Example invalid form file feedback</div>
+ <input type="file" class="form-control" aria-label="file example" required aria-describedby="validationSupportedElementsFile">
+ <div id="validationSupportedElementsFile" class="invalid-feedback">Example invalid form file feedback</div>
</div>
<div class="mb-3">
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 >}}
-<form class="row g-3 needs-validation" novalidate>
+<form class="row g-3" data-bs-toggle="form-validation" data-bs-type="tooltip" >
<div class="col-md-4 position-relative">
<label for="validationTooltip01" class="form-label">First name</label>
- <input type="text" class="form-control" id="validationTooltip01" value="Mark" required>
- <div class="valid-tooltip">
- Looks good!
- </div>
+ <input type="text" class="form-control" id="validationTooltip01" value="Mark" required data-bs-valid="Looks good!">
</div>
<div class="col-md-4 position-relative">
<label for="validationTooltip02" class="form-label">Last name</label>
- <input type="text" class="form-control" id="validationTooltip02" value="Otto" required>
- <div class="valid-tooltip">
- Looks good!
- </div>
+ <input type="text" class="form-control" id="validationTooltip02" value="Otto" required data-bs-valid="Looks good!">
</div>
<div class="col-md-4 position-relative">
<label for="validationTooltipUsername" class="form-label">Username</label>
<div class="input-group has-validation">
<span class="input-group-text" id="validationTooltipUsernamePrepend">@</span>
- <input type="text" class="form-control" id="validationTooltipUsername" aria-describedby="validationTooltipUsernamePrepend" required>
- <div class="invalid-tooltip">
- Please choose a unique and valid username.
- </div>
+ <input type="text" class="form-control" id="validationTooltipUsername" aria-describedby="validationTooltipUsernamePrepend" required data-bs-invalid="Please choose a username.">
</div>
</div>
<div class="col-md-6 position-relative">
<label for="validationTooltip03" class="form-label">City</label>
- <input type="text" class="form-control" id="validationTooltip03" required>
- <div class="invalid-tooltip">
- Please provide a valid city.
- </div>
+ <input type="text" class="form-control" id="validationTooltip03" required data-bs-invalid="Please provide a valid city.">
</div>
<div class="col-md-3 position-relative">
<label for="validationTooltip04" class="form-label">State</label>
- <select class="form-select" id="validationTooltip04" required>
+ <select class="form-select" id="validationTooltip04" required data-bs-invalid="Please select a valid state.">
<option selected disabled value="">Choose...</option>
<option>...</option>
</select>
- <div class="invalid-tooltip">
- Please select a valid state.
- </div>
</div>
<div class="col-md-3 position-relative">
<label for="validationTooltip05" class="form-label">Zip</label>
- <input type="text" class="form-control" id="validationTooltip05" required>
- <div class="invalid-tooltip">
- Please provide a valid zip.
- </div>
+ <input type="text" class="form-control" id="validationTooltip05" required data-bs-invalid="Please provide a valid zip.">
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Submit form</button>
+ <button class="btn btn-danger" type="reset">Reset form</button>
</div>
</form>
{{< /example >}}
-// 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)
- })
-})()