Skip to content

Commit 1bd6c94

Browse files
committed
Make a form validation handler | handle form messages
1 parent d826020 commit 1bd6c94

10 files changed

Lines changed: 356 additions & 105 deletions

File tree

js/index.esm.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { default as Button } from './src/button'
1010
export { default as Carousel } from './src/carousel'
1111
export { default as Collapse } from './src/collapse'
1212
export { default as Dropdown } from './src/dropdown'
13+
export { default as Form } from './src/forms/form'
1314
export { default as Modal } from './src/modal'
1415
export { default as Offcanvas } from './src/offcanvas'
1516
export { default as Popover } from './src/popover'

js/index.umd.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Alert from './src/alert'
99
import Button from './src/button'
1010
import Carousel from './src/carousel'
1111
import Collapse from './src/collapse'
12+
import Form from './src/forms/form'
1213
import Dropdown from './src/dropdown'
1314
import Modal from './src/modal'
1415
import Offcanvas from './src/offcanvas'
@@ -23,6 +24,7 @@ export default {
2324
Button,
2425
Carousel,
2526
Collapse,
27+
Form,
2628
Dropdown,
2729
Modal,
2830
Offcanvas,

js/src/forms/field.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* --------------------------------------------------------------------------
3+
* Bootstrap (v5.3.0): forms/field.js
4+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5+
* --------------------------------------------------------------------------
6+
*/
7+
8+
import { getUID, isElement } from '../util/index'
9+
import Messages from './messages'
10+
import EventHandler from '../dom/event-handler'
11+
import BaseComponent from '../base-component'
12+
import SelectorEngine from '../dom/selector-engine'
13+
import TemplateFactory from '../util/template-factory'
14+
15+
const NAME = 'field'
16+
const DATA_KEY = 'bs.field'
17+
const EVENT_KEY = `.${DATA_KEY}`
18+
const EVENT_INPUT = `input${EVENT_KEY}`
19+
const CLASS_PREFIX_ERROR = 'invalid'
20+
const CLASS_PREFIX_INFO = 'info'
21+
const CLASS_PREFIX_SUCCESS = 'valid'
22+
const CLASS_FIELD_ERROR = 'is-invalid'
23+
const CLASS_FIELD_SUCCESS = 'is-valid'
24+
25+
const ARIA_DESCRIBED_BY = 'aria-describedby'
26+
const Default = {
27+
name: null,
28+
type: 'feedback', // or tooltip
29+
valid: '', // valid message to add
30+
invalid: '' // invalid message to add
31+
}
32+
33+
const DefaultType = {
34+
name: 'string',
35+
type: 'string',
36+
valid: 'string',
37+
invalid: 'string'
38+
}
39+
40+
class Field extends BaseComponent {
41+
constructor(element, config) {
42+
super(element, config)
43+
if (!isElement(this._element)) {
44+
throw new TypeError(`field "${this._config.name}" not found`)
45+
}
46+
47+
this._tipId = getUID(`${this._config.name}-formTip-`)
48+
this._initialDescribedBy = this._element.getAttribute(ARIA_DESCRIBED_BY) || ''
49+
50+
this._errorMessages = this._getNewMessagesCollection(CLASS_PREFIX_ERROR, CLASS_FIELD_ERROR)
51+
this._helpMessages = this._getNewMessagesCollection(CLASS_PREFIX_INFO, '')
52+
this._successMessages = this._getNewMessagesCollection(CLASS_PREFIX_SUCCESS, CLASS_FIELD_SUCCESS)
53+
this._initializeMessageCollections()
54+
55+
EventHandler.on(this._element, EVENT_INPUT, () => {
56+
this.clearAppended()
57+
})
58+
}
59+
60+
static get NAME() {
61+
return NAME
62+
}
63+
64+
static get Default() {
65+
return Default
66+
}
67+
68+
static get DefaultType() {
69+
return DefaultType
70+
}
71+
72+
getElement() {
73+
return this._element
74+
}
75+
76+
getName() {
77+
return this._config.name
78+
}
79+
80+
clearAppended() {
81+
const appendedFeedback = SelectorEngine.findOne(`#${this._tipId}`, this._element.parentNode)
82+
if (!appendedFeedback) {
83+
return
84+
}
85+
86+
appendedFeedback.remove()
87+
88+
this._element.classList.remove(CLASS_FIELD_ERROR, CLASS_FIELD_SUCCESS)
89+
90+
if (this._initialDescribedBy) {
91+
this._element.setAttribute(ARIA_DESCRIBED_BY, this._initialDescribedBy)
92+
return
93+
}
94+
95+
this._element.removeAttribute(ARIA_DESCRIBED_BY)
96+
}
97+
98+
errorMessages() {
99+
return this._errorMessages
100+
}
101+
102+
helpMessages() {
103+
return this._helpMessages
104+
}
105+
106+
successMessages() {
107+
return this._successMessages
108+
}
109+
110+
appendFeedback(feedback, extraClass) {
111+
if (!feedback) {
112+
return
113+
}
114+
115+
this.clearAppended()
116+
117+
if (!(feedback instanceof TemplateFactory)) {
118+
const config = { content: { div: feedback } }
119+
feedback = new TemplateFactory(config)
120+
}
121+
122+
const feedbackElement = feedback.toHtml()
123+
feedbackElement.id = this._tipId
124+
if (extraClass) {
125+
feedbackElement.classList.add(extraClass)
126+
}
127+
128+
this._element.parentNode.append(feedbackElement)
129+
130+
const describedBy = `${this._initialDescribedBy} ${feedbackElement.id}`.trim()
131+
this._element.setAttribute(ARIA_DESCRIBED_BY, describedBy)
132+
}
133+
134+
_getNewMessagesCollection(classPrefix = '', elementClass = '') {
135+
const config = {
136+
extraClass: `${classPrefix}-${this._config.type} ${elementClass}`
137+
}
138+
return new Messages(config)
139+
}
140+
141+
_initializeMessageCollections() {
142+
if (this._config.invalid) {
143+
this.errorMessages().set('default', this._config.invalid)
144+
}
145+
146+
if (this._config.valid) {
147+
this.successMessages().set('default', this._config.valid)
148+
}
149+
}
150+
}
151+
152+
export default Field

js/src/forms/form.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* --------------------------------------------------------------------------
3+
* Bootstrap (v5.3.0): util/form-validation.js
4+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5+
* --------------------------------------------------------------------------
6+
*/
7+
import BaseComponent from '../base-component'
8+
import EventHandler from '../dom/event-handler'
9+
import Field from './field'
10+
import SelectorEngine from '../dom/selector-engine'
11+
12+
const NAME = 'formValidation'
13+
const DATA_KEY = 'bs.formValidation'
14+
const EVENT_KEY = `.${DATA_KEY}`
15+
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`
16+
const EVENT_SUBMIT = `submit${EVENT_KEY}`
17+
const EVENT_RESET = `reset${EVENT_KEY}`
18+
19+
const CLASS_VALIDATED = 'was-validated'
20+
const SELECTOR_DATA_TOGGLE = 'form[data-bs-toggle="form-validation"]'
21+
22+
const Default = {
23+
type: 'feedback' // or 'tooltip'
24+
}
25+
26+
const DefaultType = {
27+
type: 'string'
28+
}
29+
30+
class Form extends BaseComponent {
31+
constructor(element, config) {
32+
super(element, config)
33+
if (this._element.tagName !== 'FORM') {
34+
throw new TypeError(`Need to be initialized in form elements. "${this._element.tagName}" given`)
35+
}
36+
37+
this._formFields = null // Our fields
38+
}
39+
40+
static get NAME() {
41+
return NAME
42+
}
43+
44+
static get Default() {
45+
return Default
46+
}
47+
48+
static get DefaultType() {
49+
return DefaultType
50+
}
51+
52+
getFields() {
53+
if (!this._formFields) {
54+
this._formFields = this._initializeFields()
55+
}
56+
57+
return this._formFields
58+
}
59+
60+
getField(name) {
61+
return this.getFields().get(name)
62+
}
63+
64+
clear() {
65+
this._element.classList.remove(CLASS_VALIDATED)
66+
// eslint-disable-next-line no-unused-vars
67+
for (const [name, field] of this.getFields()) {
68+
field.clearAppended()
69+
}
70+
}
71+
72+
validate() {
73+
this.clear()
74+
if (this._element.checkValidity()) {
75+
return
76+
}
77+
78+
// eslint-disable-next-line no-unused-vars
79+
for (const [name, field] of this.getFields()) {
80+
const message = this._getFieldMessage(field)
81+
field.appendFeedback(message)
82+
}
83+
84+
this._element.classList.add(CLASS_VALIDATED)
85+
}
86+
87+
_getFieldMessage(field) {
88+
const element = field.getElement()
89+
90+
if (element.checkValidity()) { // if field is valid, return first success message
91+
return field.successMessages().getFirst()
92+
}
93+
94+
if (field.errorMessages().has('default')) { // if field is invalid check and return for default message
95+
return field.errorMessages().get('default')
96+
}
97+
98+
for (const property in element.validity) { // else return the default browser validation message
99+
if (element.validity[property]) {
100+
field.errorMessages().set(property, element.validationMessage)
101+
return field.errorMessages().get(property)
102+
}
103+
}
104+
105+
return ''
106+
}
107+
108+
_initializeFields() {
109+
const fields = new Map()
110+
const formElements = Array.from(this._element.elements) // the DOM elements
111+
for (const element of formElements) {
112+
const name = element.name || element.id
113+
114+
const field = Field.getOrCreateInstance(element, {
115+
name,
116+
type: this._config.type
117+
})
118+
fields.set(name, field)
119+
}
120+
121+
return fields
122+
}
123+
}
124+
125+
// On submit we want to auto-validate form
126+
EventHandler.on(document, EVENT_SUBMIT, SELECTOR_DATA_TOGGLE, event => {
127+
const { target } = event
128+
const instance = Form.getOrCreateInstance(target)
129+
if (!target.checkValidity()) {
130+
event.preventDefault()
131+
event.stopPropagation()
132+
}
133+
134+
instance.validate()
135+
})
136+
137+
EventHandler.on(document, EVENT_RESET, SELECTOR_DATA_TOGGLE, event => {
138+
const { target } = event
139+
const instance = Form.getOrCreateInstance(target)
140+
141+
instance.clear()
142+
})
143+
144+
// On load, add `novalidate` attribute to avoid browser validation
145+
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
146+
for (const el of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
147+
el.setAttribute('novalidate', true)
148+
}
149+
})
150+
export default Form
151+

js/src/forms/messages.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* --------------------------------------------------------------------------
3+
* Bootstrap (v5.3.0): forms/messages.js
4+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5+
* --------------------------------------------------------------------------
6+
*/
7+
import TemplateFactory from '../util/template-factory'
8+
9+
class Messages extends Map {
10+
constructor(templateConfig) {
11+
super()
12+
this._templateConfig = templateConfig
13+
}
14+
15+
set(key, message) {
16+
const config = { ...this._templateConfig, content: { div: message } }
17+
super.set(key, new TemplateFactory(config))
18+
}
19+
20+
getFirst() {
21+
const first = this.values().next()
22+
return first ? first.value : null
23+
}
24+
25+
count() {
26+
return this.size
27+
}
28+
}
29+
30+
export default Messages

site/content/docs/5.1/examples/checkout-rtl/index.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
direction: rtl
55
extra_css:
66
- "../checkout/form-validation.css"
7-
extra_js:
8-
- src: "../checkout/form-validation.js"
97
body_class: "bg-light"
108
---
119

@@ -67,7 +65,7 @@ <h6 class="my-0">رمز ترويجي</h6>
6765
</div>
6866
<div class="col-md-7 col-lg-8">
6967
<h4 class="mb-3">عنوان الفوترة</h4>
70-
<form class="needs-validation" novalidate>
68+
<form data-bs-toggle="form-validation">
7169
<div class="row g-3">
7270
<div class="col-sm-6">
7371
<label for="firstName" class="form-label">الاسم الأول</label>

site/content/docs/5.1/examples/checkout/form-validation.js

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)