diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e43b0f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.DS_Store
diff --git a/README.md b/README.md
index 165ee51..8922e84 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
-# Form Validation
+# Demo
+
+A demo of the form validation class can be found [here](https://demos.danremollino.dev/form-validation/).
+
+# Documentation
Importing the `Validation` class queries the current page for `
` elements
+and disables the default browser [constraint validation](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation)
+by adding a `novalidate` attribute to that element.
+
+The default behavior is to validate each field when their value is updated,
+and validate the entire form when it is submitted. This can be changed to only
+validate on form submission by setting [`onlyOnSubmit`](#onlyonsubmit) to `true` on the
+instance.
+
+Once a field is marked invalid, it will always re-validate when its value is
+changed regardless of the [`onlyOnSubmit`](#onlyOnSubmit) value.
+
+> `required` fields are tested for blank first. If this check is passed, the
+field will then test for [custom rules](#custom-validation) if present. If
+there are no custom rules, some fields are also tested further based on their
+`type` attribute value. These are described [below](#markup).
+
+## Required Fields
+
+Usually, the majority of fields in a form need validation. To make setup
+easier, instances of the *Validation* class have a default [`reqAll`](#reqall)
+value of `true`. This marks all fields that meet the following criteria
+required and needing validation:
+
+* Have a `name` attribute.
+* Not [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled).
+
+Indivudual fields can then be excluded from validation as described [below](#reqall).
+
+This functionality can be inverted by setting [`reqAll`](#reqall) to `false`,
+and manually giving your required fields a [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required)
+attribute.
+
+```html
+
+ Text
+
+
+```
+
+` ` `type` attribute values of `checkbox` and `radio` only need one option to have the
+`required` attribute, however, it is recommended to add it to all options for
+consistency. More information on checkbox and radio fields can be found [below](#checkbox-and-radio-fields).
+
+### Required Indicator
+
+A `` tag with class of `required__indicator` containing an asterisk(*)
+character will be inserted [`beforeend`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML#beforeend)
+of a required field's `` element.
+
+This indicator will be inserted to the `` element of `checkbox` and
+`radio` ` ` types if present as described [below](#checkbox-and-radio-fields).
+
+The indicator can be disabled or customized as described in [Form Options](#form-options).
+
+## Markup
+
+The `Validation` class requires the following for each required field:
+- All form fields to be wrapped in an element with the `form-group` class.
+- Do **NOT** nest the required field in a `` element.
+- Unique, matching values of the required field's `id` and its ``'s `for`
+attribute.
+- A unique `name` attribute on the required field. `checkbox` and `radio`
+` ` types should share the same unique `name` attribute.
+
+> When a field fails validation the wrapper `.form-group` element will receive
+an `invalid` CSS class. Use this to write your own CSS to indicate an invalid
+state.
+
+```html
+
+ Text
+
+
+```
+
+### Checkbox and Radio Fields
+
+- Wrap all options in a `` element and add the `form-group` class to
+it instead of the individual options.
+- The `` tag may be used to give the option set a heading.
+- Each `checkbox` or `radio` ` ` type should be wrapped in a
+`form-group__item` class. This class is not used programattically, and is only
+used for styling.
+
+```html
+
+ Check Group
+
+
+ Checkbox 1
+
+
+
+ Checkbox 2
+
+
+```
+
+### Select Fields
+
+Required `` elements should contain an `` with the `selected`
+attribute and an empty `value` attribute. If not present the element will pass
+validation even while being required.
+
+```html
+
+ Select
+
+ Choose an Item
+ 1
+ 2
+ 3
+
+
+```
+
+### Password Fields
+
+When an ` ` element with a `type` attribute of `password` is detected on the
+page, a required *Password Confirm* field will auto generate after its parent
+`.form-group` wrapper.
+
+The markup for this field is a duplicate of the original input's parent
+`.form-group` wrapper. The `id` and `name` attributes of the ` ` element,
+and the `for` attribute of the `` element will have their values set to
+`password-confirm`.
+
+This functionality can be disabled by settting [`passConfirm`](#passConfirm) to `false`.
+
+A *Password* field with the markup of
+
+```html
+
+ Password
+
+
+```
+
+will generate a *Password Confirm* field with the markup of
+
+```html
+
+ Password Confirm*
+
+
+```
+
+> The *Password* field is tested that it matches its corresponding
+*Password Confirm* field when the *Password Confirm* field contains a non-blank
+value.
+
+The *Password Confirm* field is always tested that it matches its corresponding
+*Password* field.
+
+## Custom Form Controls
+
+[Custom form controls](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls)
+can be validated by informing the ValidationField class what you want to check
+against as the value for your control. You can determine validity based on:
+
+1. [Existence of an element.](#existence-of-an-element)
+2. [TextContent of an element.](#textcontent-of-an-element)
+3. [Existence of an attribute.](#existence-of-an-attribute)
+4. [Value of an attribute.](#value-of-an-attribute)
+
+To validate a custom form control, add a `data-validation` attribute to an
+element of the control. The value should be a single quoted JSON object with
+`valueSrc` as a property.
+
+> The element that the `data-validation` attribute is added to **MUST** have a
+unique `id` and `name` attribute. Having an associated `label`/`legend` tag
+with a `for` attribute is optional, but recommended.
+
+The value of the property should be an array with up to three items.
+
+`valueSrc[0]:` A CSS selector to target the element to reference for value.
+`valueSrc[1]:` The keyword `attribute`, `attributeValue`, `exists`, or
+`textContent`.
+`valueSrc[2]:` If using `attribute` or `attributeValue`, the attribute to
+reference. If using `exists`, a CSS selector relative to `valueSrc[0]` as the
+parent.
+
+`valueSrc` can be combined with [`pattern`](#pattern) and [`patternMessage`](#patternmessage)
+field options to check for specific values and output custom error messages.
+See the examples below.
+
+### Existence of an Element
+
+Determine validity based on whether an element exists or not.
+
+*Valid if...* the target element exists.
+
+The following example checks for the presence of in image element that is a
+child of the element with an id of 'custom-control'.
+
+```html
+
+```
+
+> The third item in the `valueSrc` array can be any valid CSS selector string
+that [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
+can parse. e.x. `:scope > img`
+
+### TextContent of an Element
+
+Use the target element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
+to determine validity. Useful when using [`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
+
+*Valid if...* the target element's textContent property is a non-empty string.
+
+The following example checks if the element with an id of 'custom-control' has
+a non-empty string [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
+property.
+
+```html
+
+```
+
+### Existence of an Attribute
+
+Use the target element's specified attribute existence to determine validity.
+
+*Valid if...* the target element's specified attribute exists.
+
+The following example checks if the element with an id of 'custom-control' has
+a `title` attribute.
+
+```html
+
+```
+
+> The value of a [boolean attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)
+will evaluate as `false` and invalid unless it is a non-empty string. When
+using this option, the attribute must use the `checked="checked"` (true, valid)
+and `checked=""` (false, invalid) form and not `checked`. `checked` will
+evaluate as false and invalid whether present or not.
+
+### Value of an Attribute
+
+Use the target element's specified attribute value to determine validity.
+
+*Valid if...* the value of the target's specified attribute is a non-empty string.
+
+The following example checks if the element with an id of 'custom-control' has
+a `title` attribute with a non-empty string value.
+
+```html
+
+```
+
+> The value of a [boolean attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)
+will evaluate as `false` and invalid unless it is a non-empty string. When
+using this option, the attribute must use the `checked="checked"` (true, valid)
+and `checked=""` (false, invalid) syntax. If you need to check a boolean
+attribute use [`attribute`](#existence-of-an-attribute).
+
+## Form Options
+
+* [invalidClass](#invalidclass)
+* [messageClass](#messageclass)
+* [onlyOnSubmit](#onlyonsubmit)
+* [onSuccess](#onsuccess)
+* [passConfirm](#passconfirm)
+* [reqAll](#reqall)
+* [reqIndicator](#reqindicator)
+* [reqIndicators](#reqindicators)
+* [reqIndicatorClass](#reqindicatorclass)
+* [valFieldsOn](#valfieldson)
+
+Default instance options can be overwritten by adding a single quoted
+`data-validation` attribute on the `` being validated. Its value must be
+a properly formatted JSON object.
+
+```html
+
+
+ Text
+
+
+
+ Submit
+
+
+```
+
+### invalidClass
+
+**Default:** `invalid`
+
+The class assigned to the parent `
` wrapper of the
+invalid field.
+
+### messageClass
+
+**Default:** `invalid__message`
+
+The class assigned output message of the invalid field.
+
+### onlyOnSubmit
+
+**Default:** `false`
+
+Only validate the form on submission. A setting of `true` will disable
+individual field validation. Regardless of this setting fields will always
+validate on the [`valFieldsOn`](#valFieldsOn) value while they are invalid.
+
+### onSuccess
+
+**Default:** `null`
+
+Run a script when the form is submited and all fields pass validation. A non
+truthy value will cancel the form's default action.
+
+```html
+...
+```
+
+### passConfirm
+
+**Default:** `true`
+
+Automatically create a confirm password field. The markup from the user created
+field with a `type="password"` attribute will be duplicated and modifed, then
+inserted after the original field.
+
+```html
+...
+```
+
+### reqAll
+
+**Default:** `true`
+
+Automatically add a `required` attribute to all ` `, ``, and
+`` tags in the ``. This allows you to skip marking individual
+fields as `required` in your markup.
+
+> Individual fields can be excluded from validation by adding a
+`data-validation` attribute to a field that's value is a JSON object with an
+[`optional`](#optional) property value set to `true`.
+
+```html
+
+ Not Required
+
+
+```
+
+### reqIndicator
+
+**Default:** `*`
+
+The string to render after the label text to indicate to the user that the
+field is required.
+
+### reqIndicators
+
+**Default:** `true`
+
+Enable the output of `reqIndicator` strings.
+
+If set to `false`, you can target all field labels with the CSS selector below
+to apply a custom style.
+
+```css
+.form-group:has([required])
+:where(label:has(:not(.form-group__item)), legend) {
+ /* Your styles here. */
+}
+```
+
+### reqIndicatorClass
+
+**Default:** `required__indicator`
+
+The class assigned to the `` wrapper of the outputted `reqIndicator`.
+
+### valFieldsOn
+
+**Default:** `input`
+
+The event that triggeers individual field validation. Value can be any
+JavaScript event, but it is recommended to try
+[`blur`](https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event),
+[`change`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event),
+or [`input`](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event).
+
+> Individual field validation can be disable by setting
+[`onlyOnSubmit`](#onlyOnSubmit) to true.
+
+## Field Options
+
+* [optional](#optional)
+* [pattern](#pattern)
+* [patternMessage](#patternmessage)
+* [valueSrc](#valuesrc)
+
+Individual fields can be passed options to customize their behavior by adding a
+single quoted `data-validation` attribute to a field. Its value must be a
+properly formatted JSON object with the properties and values below.
+
+```html
+
+ Custom Regex
+
+
+```
+
+### optional
+
+**Default:** `false`
+
+Fields with the `optional` property set to `true` will pass vaidation when
+blank. They still will be processed for validation when they contain a value
+based on the `input` `type` attribute, or if a user [`pattern`](#pattern) is specified.
+
+### pattern
+
+**Default:** `null`
+
+A regular expression to test the field's value against. This string is also
+copied and added to the field as a native [`pattern`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern)
+attribute. It is recommended to also provide an error message to display when
+the value does not match `pattern` by providing a [`patternMessage`](#patternmessage).
+
+> The pattern is checked when input is present. A required field will always
+first test for blank, then for the provided pattern.
+
+### patternMessage
+
+**Default:** `null`
+
+An error message to display when the field fails the custom validation test
+provided by [`pattern`](#pattern).
+
+> If not provided with [`pattern`](#pattern) the field will be marked as
+invalid, but no message will output when the pattern does not match.
+
+### valueSrc
+
+**Default:** `null`
+
+Used to validate [custom form controls](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls).
+See [here](#custom-form-controls) for more information.
diff --git a/demo/js/validation/validation.js b/demo/js/validation/validation.js
new file mode 100644
index 0000000..f9ee273
--- /dev/null
+++ b/demo/js/validation/validation.js
@@ -0,0 +1,450 @@
+class ValidationField {
+ el = null
+ formGroupEl = null
+ isValid = null
+ labelEl = null
+ labelText = null
+ message = null
+ messageOutputted = null
+ optional = false
+ pattern = null
+ patternMessage = null
+ validationInstance = null
+ valueSrc = [] // [css_selector, 'attribute|attributeValue'|'exists'|'textContent', attribute_name|element]
+
+ constructor(obj) {
+ try {
+ Object.assign(this, obj)
+
+ this.formGroupEl = this.el.closest('.form-group')
+ this.labelEl = this.formGroupEl?.querySelector('legend') ??
+ this.validationInstance.form.querySelector(
+ `[name="${this.el.getAttribute('name')}"]:not([type="hidden"])`
+ ).closest('.form-group').querySelector('label')
+ this.labelText = this.labelEl?.textContent.replace(this.validationInstance.reqIndicator, '')
+
+ // user field config
+ try {
+ Object.assign(this, this.el.dataset.validation ? JSON.parse(this.el.dataset.validation) : {})
+ } catch(error) {
+ if (error instanceof SyntaxError) {
+ console.warn(`There was a problem configuring your field options. Using defaults. Are you using a properly formatted JSON object? (e.g. data-validation='{"optional": true}')\n\nElement: ${this.el.outerHTML}`)
+ }
+ delete this.el.dataset.validation
+ }
+
+ // custom form controls
+ if (this.valueSrc.length > 0) {
+ const valueProps = {
+ valueRef: this.validationInstance.form.querySelector(this.valueSrc[0]),
+ kind: this.valueSrc[1],
+ attr: this.valueSrc[2]
+ }
+
+ let observerOpts
+ const updateValue = (valueProps) => {
+ switch (valueProps.kind) {
+ case 'attribute':
+ valueProps.valueRef.hasAttribute(valueProps.attr)
+ ? this.el.setAttribute('value', '1')
+ : this.el.setAttribute('value', '')
+ observerOpts = {attributeFilter: [valueProps.attr]}
+ break
+ case 'attributeValue':
+ valueProps.valueRef.getAttribute(valueProps.attr)
+ ? this.el.setAttribute('value', valueProps.valueRef.getAttribute(valueProps.attr))
+ : this.el.setAttribute('value', '')
+ observerOpts = {attributeFilter: [valueProps.attr]}
+ break
+ case 'exists':
+ valueProps.valueRef.querySelector(valueProps.attr)
+ ? this.el.setAttribute('value', '1')
+ : this.el.setAttribute('value', '')
+ observerOpts = {childList: true}
+ break
+ case 'textContent':
+ this.el.setAttribute('value', valueProps.valueRef?.textContent.trim())
+ observerOpts = {characterData: true, childList: true, subtree: true}
+ break
+ }
+ }
+
+ updateValue(valueProps)
+
+ const custConObserver = new MutationObserver((mutationList, observer) => {
+ updateValue(valueProps)
+ this.validationInstance.validate(this)
+ })
+
+ custConObserver.observe(valueProps.valueRef, observerOpts)
+
+ // focus on the value target when the label is clicked
+ this.labelEl.addEventListener('click', () => {
+ valueProps.valueRef.focus()
+ })
+ }
+
+ // validate individual fields according to the parent Validation
+ // instance `valFieldsOn` value
+ this.el.addEventListener(this.validationInstance.valFieldsOn, () => {
+ this.validationInstance.validate(this)
+ })
+
+ // add a native pattern attr if pattern property is present
+ if (this.pattern) this.el.setAttribute('pattern', this.pattern)
+
+ // add required indicator
+ if (this.validationInstance.reqIndicators) {
+ const hasIndicator = this.formGroupEl?.querySelector(`.${this.validationInstance.reqIndicatorClass}`)
+ if (!hasIndicator && !this.optional) {
+ this.labelEl?.insertAdjacentHTML(
+ 'beforeend',
+ `${this.validationInstance.reqIndicator} `
+ )
+ }
+ }
+
+ // validate the field without outputting errors to set the default state
+ this.validationInstance.validate(this, false, true)
+
+ // console.log(this)
+ } catch (error) {
+ console.warn(`Something when wrong setting up a ValidationField instance.`)
+ }
+ }
+}
+
+class Validation {
+ static forms = []
+
+ errors = []
+ form = null
+ invalidClass = 'invalid'
+ messageClass = 'invalid__message'
+ onlyOnSubmit = false
+ onSuccess = null
+ passConfirm = true
+ passConfirmField = null
+ passwordField = null
+ reqAll = true
+ reqIndicator = '*'
+ reqIndicators = true
+ reqIndicatorClass = 'required__indicator'
+ reqData = new Set()
+ reqFields = []
+ valFieldsOn = 'input' // blur, change, input
+
+ constructor(obj) {
+ Object.assign(this, obj)
+
+ // check for custom form controls
+ const custFormControls = this.form.querySelectorAll('[data-validation]:not(form, input, select, textarea)')
+ if (custFormControls.length > 0) {
+ for (const el of custFormControls) {
+ try {
+ const fieldOptions = JSON.parse(el.dataset.validation)
+ if (!this.form.querySelector(fieldOptions.valueSrc[0])) {
+ throw new Error(`No element matched with the supplied selector – ${fieldOptions.valueSrc[0]}`)
+ }
+
+ let inputEl = document.createElement('input')
+ inputEl.dataset.validation = el.dataset.validation
+ let inputAttrs = {name: el.getAttribute('name'), type: 'hidden', value: ''}
+ for (const [key, value] of Object.entries(inputAttrs)) {
+ inputEl[key] = value
+ }
+
+ el.parentElement.prepend(inputEl)
+ } catch (error) {
+ console.warn(`There was a problem configuring your custom form control. Aborted.\n\nError: ${error.message}.\n\nElement: ${el.outerHTML}`)
+ }
+ }
+ }
+
+ // add confirm password field if a password field is present
+ if (this.passConfirm) {
+ const pwField = this.form.querySelector('input[type="password"]')
+ if (pwField) {
+ this.passwordField = new ValidationField({el: pwField, validationInstance: this})
+ const passConfirmGroup = this.passwordField.formGroupEl.cloneNode(true)
+ for (const child of passConfirmGroup.children) {
+ switch (child.tagName) {
+ case 'LABEL':
+ child.innerText = 'Password Confirm'
+ child.htmlFor = 'password-confirm'
+ break
+ case 'INPUT':
+ child.setAttribute('id', 'password-confirm')
+ child.setAttribute('name', 'password-confirm')
+ break
+ }
+ }
+ this.passwordField.formGroupEl.insertAdjacentHTML('afterend', passConfirmGroup.outerHTML)
+ this.passConfirmField = new ValidationField({el: this.form.querySelector('input[name="password-confirm"]'), validationInstance: this})
+ }
+ }
+
+ // add required attr to fields if reqAll == true
+ if (this.reqAll) {
+ for (const field of this.form.querySelectorAll('input, select, textarea')) {
+ field.setAttribute('required', 'required')
+ }
+ }
+
+ // check for fields that are elidgeable for validation and make a set of unique name attr values
+ for (const field of this.form.querySelectorAll(':where(input, select, textarea)[required][name]:not([disabled])')) {
+ this.reqData.add(field.name)
+ }
+
+ // create a list of all the field elements that were specified for validation
+ for (const nameAttr of this.reqData) {
+ const fields = this.form.querySelectorAll(`[name="${nameAttr}"]:where(input, select, textarea)`)
+ for (const el of fields) {
+ this.reqFields.push(new ValidationField({el: el, validationInstance: this}))
+ }
+ }
+
+ this.form.addEventListener('submit', e => {
+ e.preventDefault()
+ this.validate()
+ })
+
+ this.form.addEventListener('reset', () => {
+ this.removeErrors()
+ })
+
+ // add/remove a field when disabled attr is dynamically toggled
+ const observeEls = this.form.querySelectorAll('input, select, textarea, [data-validation]:not(form, input, select, textarea)')
+ const disabledObserver = new MutationObserver((mutationList, observer) => {
+ for (const item of mutationList) {
+ let updateEl = item.target
+
+ if (item.target.dataset.validation?.includes('valueSrc')) {
+ // custom form control – update the hidden input instead
+ updateEl = this.form.querySelector(
+ `input[name="${item.target.getAttribute('name')}"][type="hidden"]`
+ )
+ }
+
+ updateEl.hasAttribute('disabled')
+ ? this.removeField(updateEl)
+ : this.addField(updateEl)
+ }
+ })
+ for (const el of observeEls) {
+ disabledObserver.observe(el, {
+ attributes: true,
+ attributeFilter: ['disabled'],
+ attributeOldValue: true
+ })
+ }
+
+ // console.log(this)
+ }
+
+ // add a field after initialization
+ addField(el) {
+ this.reqData.add(el.name)
+ this.reqFields.push(new ValidationField({el: el, validationInstance: this}))
+ }
+
+ // remove a field after initialization
+ removeField(el) {
+ this.reqData.delete(el.name)
+ for (const obj of this.reqFields) {
+ if (el === obj.el) {
+ obj.formGroupEl.querySelector(`.${this.reqIndicatorClass}`)?.remove()
+ this.reqFields.pop(obj)
+ this.removeErrors(obj)
+ }
+ }
+ }
+
+ parseField(fieldInstance) {
+ const nameAttrVal = fieldInstance.el.name
+
+ if (!fieldInstance.optional) {
+ if (!fieldInstance.el.value) {
+ this.validateForBlank(fieldInstance)
+ } else {
+ if (fieldInstance.pattern) {
+ this.validatePattern(fieldInstance)
+ } else {
+ switch (fieldInstance.el.type) {
+ case 'checkbox':
+ this.validateCheckbox(fieldInstance, nameAttrVal)
+ break
+ case 'email':
+ this.validateEmail(fieldInstance)
+ break
+ case 'password':
+ if (fieldInstance.el.name === 'password') this.validatePassword(fieldInstance)
+ if (fieldInstance.el.name === 'password-confirm') this.validateConfirmPass(fieldInstance)
+ break
+ case 'radio':
+ this.validateRadio(fieldInstance, nameAttrVal)
+ break
+ case 'tel':
+ this.validateTel(fieldInstance)
+ break
+ }
+ }
+ }
+ }
+ }
+
+ validateForBlank(fieldInstance) {
+ this.addError(
+ fieldInstance,
+ (fieldInstance.labelText ? fieldInstance.labelText : `Field`) + ` is required.`
+ )
+ }
+
+ validatePattern(fieldInstance) {
+ const regex = new RegExp(fieldInstance.pattern)
+ if (!regex.test(fieldInstance.el.value)) {
+ this.addError(fieldInstance, fieldInstance.patternMessage)
+ }
+ }
+
+ validateCheckbox(fieldInstance, nameAttrVal) {
+ if (!this.form.querySelector(`[name="${nameAttrVal}"]:checked`)) {
+ this.addError(fieldInstance, `At least one choice is required.`)
+ }
+ }
+
+ validateEmail(fieldInstance) {
+ const regex = new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
+ if (!regex.test(fieldInstance.el.value)) {
+ this.addError(fieldInstance, `'${fieldInstance.el.value}' is not a valid email address.`)
+ }
+ }
+
+ validatePassword(fieldInstance) {
+ if (this.passConfirmField.el.value) {
+ if (this.passConfirmField.el.value !== this.passwordField.el.value) {
+ this.validate(this.passConfirmField)
+ } else {
+ this.removeErrors(this.passConfirmField)
+ }
+ }
+
+ // minimum eight characters, at least one upper case English letter,
+ // one lower case English letter, one number and one special character
+ const regex = new RegExp(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,}$/)
+ if (!regex.test(fieldInstance.el.value)) {
+ this.addError(fieldInstance, `Password is not secure.`)
+ }
+ }
+
+ validateConfirmPass(fieldInstance) {
+ if (fieldInstance.el.value !== this.passwordField.el.value) {
+ this.removeErrors(fieldInstance)
+ this.addError(fieldInstance, `Does not match Password field.`)
+ }
+ }
+
+ validateRadio(fieldInstance, nameAttrVal) {
+ if (!this.form.querySelector(`[name="${nameAttrVal}"]:checked`)) {
+ this.addError(fieldInstance, `A choice is required.`)
+ }
+ }
+
+ validateTel(fieldInstance) {
+ // 1234567890, 123-456-7890, (123)456-7890, (123) 456-7890
+ const regex = new RegExp(/^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$/)
+ if (!regex.test(fieldInstance.el.value)) {
+ this.addError(fieldInstance, `'${fieldInstance.el.value}' is not a valid telephone number.`)
+ }
+ }
+
+ addError(fieldInstance, message) {
+ fieldInstance.isValid = false
+ fieldInstance.message = message
+ this.errors.push(fieldInstance)
+ }
+
+ outputErrors(fieldInstance) {
+ for (const error of this.errors) {
+ if (!fieldInstance || error.el === fieldInstance.el) {
+ error.formGroupEl.classList.add(`${this.invalidClass}`)
+ if (error.message) {
+ error.el.insertAdjacentHTML(
+ 'beforeBegin',
+ `${error.message}
`
+ )
+ }
+ error.messageOutputted = true
+ }
+ }
+ }
+
+ removeErrors(fieldInstance) {
+ const updateMarkup = valField => {
+ valField.formGroupEl.classList.remove(`${this.invalidClass}`)
+ valField.formGroupEl.querySelector(`.${this.messageClass}`)?.remove()
+ }
+
+ if (fieldInstance) {
+ for (const [index, error] of this.errors.entries()) {
+ if (error.el.name === fieldInstance.el.name) {
+ this.errors.splice(index, 1)
+ }
+ }
+ updateMarkup(fieldInstance)
+ fieldInstance.isValid = true
+ fieldInstance.messageOutputted = false
+ } else {
+ for (const error of this.errors) {
+ updateMarkup(error)
+ }
+ this.errors.length = 0
+ }
+ }
+
+ validate(fieldInstance, outputErrors = true, force = false) {
+ if (fieldInstance) {
+ if (!this.onlyOnSubmit || (this.onlyOnSubmit && fieldInstance.messageOutputted) || force) {
+ this.removeErrors(fieldInstance)
+ this.parseField(fieldInstance)
+ if (outputErrors) this.outputErrors(fieldInstance)
+ }
+ } else {
+ if (this.errors.length > 0) this.removeErrors()
+ for (const nameAttrVal of this.reqData) {
+ for (const field of this.reqFields) {
+ if (field.el.name === nameAttrVal) {
+ this.parseField(field)
+ break
+ }
+ }
+ }
+ this.outputErrors()
+ }
+
+ // console.log(this); return;
+
+ // form was sbmitted and all fields pass validation
+ if (!fieldInstance && this.errors.length === 0) {
+ this.onSuccess ? Function(this.onSuccess)() : this.form.submit()
+ }
+ }
+
+ static {
+ const forms = document.getElementsByTagName('form')
+ for (let form of forms) {
+ form.noValidate = true
+ try {
+ const config = Object.assign(
+ {form: form},
+ form.dataset.validation ? JSON.parse(form.dataset.validation) : {}
+ )
+ Validation.forms.push(new Validation(config))
+ } catch(error) {
+ if (error instanceof SyntaxError) console.warn(`There was a problem configuring your custom validation options. Using defaults. Are you using a properly formatted JSON object? (e.g. data-validation='{"reqIndicators": false}')`)
+ delete form.dataset.validation
+ Validation.forms.push(new Validation({form: form}))
+ }
+ }
+ }
+}