fixed optional fields not validating while having a value

This commit is contained in:
2025-04-04 18:07:59 -04:00
parent 50af7ae33a
commit 4e216a197f
15 changed files with 102 additions and 1656 deletions

View File

@ -1,3 +1,7 @@
# Overview
This class easily adds front-end validation to HTML forms.
# Demo
A demo of the form validation class can be found [here](https://demos.danremollino.dev/form-validation/).

View File

@ -1,94 +0,0 @@
:root {
/* ###### ##### ## ##### ###### */
/* ### ## ## ## ## ## ## ## */
/* ## ## ## ## ## ## ###### */
/* ### ## ## ## ## ## ## ## */
/* ###### ##### ####### ##### ## ## */
/*
use color-mix() with transparent to add transparency
https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix#adding_transparency
*/
&[data-color-scheme="light"] {
/* light gray */
--color-01-l: 90%;
--color-01-a: 0%;
--color-01-b: 0%;
/* black */
--color-02-l: 0%;
--color-02-a: 0%;
--color-02-b: 0%;
}
&[data-color-scheme="dark"] {
/* black */
--color-01-l: 0%;
--color-01-a: 0%;
--color-01-b: 0%;
/* light gray */
--color-02-l: 90%;
--color-02-a: 0%;
--color-02-b: 0%;
}
--color-01: oklab(var(--color-01-l) var(--color-01-a) var(--color-01-b));
--color-01-light: oklab(calc(var(--color-01-l) + 25%) var(--color-01-a) var(--color-01-b));
--color-01-dark: oklab(calc(var(--color-01-l) - 25%) var(--color-01-a) var(--color-01-b));
--color-02: oklab(var(--color-02-l) var(--color-02-a) var(--color-02-b));
--color-02-light: oklab(calc(var(--color-02-l) + 25%) var(--color-02-a) var(--color-02-b));
--color-02-dark: oklab(calc(var(--color-02-l) - 25%) var(--color-02-a) var(--color-02-b));
--link-color-01: var(--text-color-01);
--link-decoration-01: none;
--link-hover-color-01: var(--text-color-01);
--link-hover-decoration-01: underline;
--success-color: oklab(60.735% -40.534% 31.334%);
--info-color: oklab(61.465% -23.829% -17.445%);
--warning-color: oklab(82.103% 10.93% 66.106%);
--danger-color: oklab(50.864% 51.073% 25.414%);
/* ###### ###### ##### ###### ###### ## ## ###### */
/* ## ## ## ## ## ### ## ### ## ## */
/* ##### ###### ####### ## ## ## # ## ## ### */
/* ## ## ## ## ### ## ## ### ## ## */
/* ###### ## ## ## ###### ###### ## ## ##### */
--space: 1lh;
--eighth-space: calc(var(--space) * .125);
--quarter-space: calc(var(--space) * .25);
--half-space: calc(var(--space) * .5);
--three-quarter-space: calc(var(--space) * .75);
--one-and-quarter-space: calc(var(--space) * 1.25);
--one-and-half-space: calc(var(--space) * 1.5);
--one-and-three-quarter-space: calc(var(--space) * 1.75);
--double-space: calc(var(--space) * 2);
--tripple-space: calc(var(--space) * 3);
/* ###### ###### ####### ###### ## ## ###### */
/* ## ## ## ## ### ## ## */
/* ##### ## # ## ## # ## ## ### */
/* ## ## ## ## ## ### ## ## */
/* ###### ###### ####### ###### ## ## ##### */
--site-max-width: 1440px;
/* grid.css */
--content-gap: var(--double-space);
--content-padding: clamp(var(--space), 5dvw, var(--tripple-space));
--site-padding: max(5dvw, 10px);
--max-content-width: calc(var(--site-max-width) - var(--content-padding) * 2);
}

View File

@ -1,24 +0,0 @@
@layer dialog {
[data-open-dialog]:hover { cursor: pointer; }
dialog {
border: 0;
padding: var(--half-space);
position: relative;
&::backdrop {
background-color: rgb(0 0 0 / .75);
}
.close-btn:hover { cursor: pointer; }
.close-icon {
height: 20px;
inset: var(--quarter-space) var(--quarter-space) auto auto;
position: absolute;
width: 20px;
}
body:has(&[open]) { overflow: hidden; }
}
}

View File

@ -1,94 +0,0 @@
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 100;
src: url('../fonts/kommon_grotesk_thin.woff') format('woff'),
url('../fonts/kommon_grotesk_thin.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 100;
font-style: italic;
src: url('../fonts/kommon_grotesk_thin_italic.woff') format('woff'),
url('../fonts/kommon_grotesk_thin_italic.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 200;
src: url('../fonts/kommon_grotesk_extra_light.woff') format('woff'),
url('../fonts/kommon_grotesk_extra_light.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 200;
font-style: italic;
src: url('../fonts/kommon_grotesk_extra_light_italic.woff') format('woff'),
url('../fonts/kommon_grotesk_extra_light_italic.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 300;
src: url('../fonts/kommon_grotesk_light.woff') format('woff'),
url('../fonts/kommon_grotesk_light.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 300;
font-style: italic;
src: url('../fonts/kommon_grotesk_light_italic.woff') format('woff'),
url('../fonts/kommon_grotesk_light_italic.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 400;
src: url('../fonts/kommon_grotesk_normal.woff') format('woff'),
url('../fonts/kommon_grotesk_normal.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 400;
font-style: italic;
src: url('../fonts/kommon_grotesk_italic.woff') format('woff'),
url('../fonts/kommon_grotesk_italic.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 500;
src: url('../fonts/kommon_grotesk_medium.woff') format('woff'),
url('../fonts/kommon_grotesk_medium.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 600;
src: url('../fonts/kommon_grotesk_semi_bold.woff') format('woff'),
url('../fonts/kommon_grotesk_semi_bold.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 700;
src: url('../fonts/kommon_grotesk_bold.woff') format('woff'),
url('../fonts/kommon_grotesk_bold.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 800;
src: url('../fonts/kommon_grotesk_extra_bold.woff') format('woff'),
url('../fonts/kommon_grotesk_extra_bold.woff2') format('woff2');
}
@font-face {
font-family: 'Kommon Grotesk';
font-weight: 900;
src: url('../fonts/kommon_grotesk_black.woff') format('woff'),
url('../fonts/kommon_grotesk_black.woff2') format('woff2');
}

View File

@ -1,105 +0,0 @@
/* temp */
section {
/* background: gray; */
margin-block: var(--space);
}
:where(header, section, footer) > * {
background-image: url('https://res.cloudinary.com/demo/image/upload/cashew_chicken.jpg');
background-size: cover;
color: var(--color-01);
/* text-shadow: 1px 1px 3px var(--color-02); */
}
:where(header, section, footer) {
display: grid;
row-gap: var(--content-gap);
grid-template-columns:
[viewport-start] 1fr
[site-padding-start] var(--site-padding)
[content-padding-start] var(--content-padding)
[content-start]
minmax(0, calc(var(--max-content-width) / 12))
[one-twelfth]
minmax(0, calc(var(--max-content-width) / 12))
[two-twelfths one-sixth]
minmax(0, calc(var(--max-content-width) / 12))
[three-twelfths one-fourth fourth one-quarter]
minmax(0, calc(var(--max-content-width) / 12))
[four-twelfths one-third]
minmax(0, calc(var(--max-content-width) / 12))
[five-twelfths]
minmax(0, calc(var(--max-content-width) / 12))
[six-twelfths one-half half]
minmax(0, calc(var(--max-content-width) / 12))
[seven-twelfths]
minmax(0, calc(var(--max-content-width) / 12))
[eight-twelfths two-thirds]
minmax(0, calc(var(--max-content-width) / 12))
[nine-twelfths three-fourths third three-quarters]
minmax(0, calc(var(--max-content-width) / 12))
[ten-twelfths five-sixths]
minmax(0, calc(var(--max-content-width) / 12))
[eleven-twelfths]
minmax(0, calc(var(--max-content-width) / 12))
[content-end twelve-twelfths]
var(--content-padding) [content-padding-end]
var(--site-padding) [site-padding-end]
1fr [viewport-end];
> * {
display: grid;
grid-column: content-padding;
grid-template-columns: subgrid;
padding: var(--content-padding) 0;
> * { grid-column: content; }
&.full-viewport { grid-column: viewport !important; }
&.half-content-left {
grid-column-end: half !important;
padding: var(--content-padding) var(--content-padding) var(--content-padding) 0;
}
&.half-content-right {
grid-column-start: half !important;
padding: var(--content-padding) 0 var(--content-padding) var(--content-padding);
> * { grid-column-start: inherit; }
}
&.half-viewport-left {
grid-column: viewport-start / half !important;
padding: var(--content-padding) var(--content-padding) var(--content-padding) 0;
}
&.half-viewport-right {
grid-column: half / viewport-end !important;
padding: var(--content-padding) 0 var(--content-padding) var(--content-padding);
> * { grid-column-start: inherit; }
}
&.remove-padding {
grid-column: content;
padding: 0;
}
&.remove-inline-padding {
grid-column: content;
padding: var(--content-padding) 0;
}
@media (max-width: 1240px) {
&[class*="-left"] { grid-column-end: three-quarters !important; }
&[class*="-right"] { grid-column-start: one-quarter !important; }
}
@media (max-width: 940px) {
&[class*="-left"] { grid-column-end: five-sixths !important; }
&[class*="-right"] { grid-column-start: one-sixth !important; }
}
@media (max-width: 640px) {
&[class*="-left"] { grid-column-end: content-end !important; }
&[class*="-right"] { grid-column-start: content-start !important; }
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
@import './reset.css' layer(reset);
@import './custom_properties.css' layer(custom-properties);
@import './tags.css' layer(tags);
@import './grid.css' layer(grid);
@import './utility_classes.css' layer(classes);
@import './fonts.css';

View File

@ -1,114 +0,0 @@
/***
The new CSS reset - version 1.11.3 (last updated 25.08.2024)
GitHub page: https://github.com/elad2412/the-new-css-reset
***/
/*
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
- The "symbol *" part is to solve Firefox SVG sprite bug
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
*/
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
all: unset;
display: revert;
}
/* Preferred box-sizing value */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Fix mobile Safari increase font-size on landscape mode */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
/* Reapply the pointer cursor for anchor tags */
a, button {
cursor: revert;
}
/* Remove list styles (bullets/numbers) */
ol, ul, menu, summary {
list-style: none;
}
/* Firefox: solve issue where nested ordered lists continue numbering from parent (https://bugzilla.mozilla.org/show_bug.cgi?id=1881517) */
ol {
counter-reset: revert;
}
/* For images to not be able to exceed their container */
img {
max-inline-size: 100%;
max-block-size: 100%;
}
/* removes spacing between cells in tables */
table {
border-collapse: collapse;
}
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
input, textarea {
-webkit-user-select: auto;
}
/* revert the 'white-space' property for textarea elements on Safari */
textarea {
white-space: revert;
}
/* minimum style to allow to style meter element */
meter {
-webkit-appearance: revert;
appearance: revert;
}
/* preformatted text - use only for this feature */
:where(pre) {
all: revert;
box-sizing: border-box;
}
/* reset default text opacity of input placeholder */
::placeholder {
color: unset;
}
/* fix the feature of 'hidden' attribute.
display:revert; revert to element instead of attribute */
:where([hidden]) {
display: none;
}
/* revert for bug in Chromium browsers
- fix for the content editable attribute will work properly.
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
:where([contenteditable]:not([contenteditable="false"])) {
-moz-user-modify: read-write;
-webkit-user-modify: read-write;
overflow-wrap: break-word;
-webkit-line-break: after-white-space;
-webkit-user-select: auto;
}
/* apply back the draggable feature - exist only in Chromium and Safari */
:where([draggable="true"]) {
-webkit-user-drag: element;
}
/* Revert Modal native behavior */
:where(dialog:modal) {
all: revert;
box-sizing: border-box;
}
/* Remove details summary webkit styles */
::-webkit-details-marker {
display: none;
}

View File

@ -1,166 +0,0 @@
/* debug */
/* *, *::before, *::after {
outline: 2px solid lime;
} */
:root {
interpolate-size: allow-keywords;
scrollbar-color: color-mix(in oklab, var(--color-02) 40%, transparent)
color-mix(in oklab, var(--color-01) 40%, transparent);
scrollbar-gutter: stable;
}
body {
background-color: var(--color-01);
color: var(--color-02);
font: normal normal 300 clamp(1.6rem, 1.4dvw, 2.0rem)/1.25 'Helvetica', sans-serif;
letter-spacing: .25px;
overflow-x: clip;
}
button {
background: var(--color-01);
border: 1px solid var(--color-02);
color: var(--color-02);
padding: var(--quarter-space);
&:not(:last-child) { margin-bottom: var(--half-space); }
&:hover { cursor: pointer; }
}
em {
font-style: italic;
font-weight: bolder;
}
figcaption {
font-size: smaller;
}
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
html {
font-size: 62.5%;
scroll-behavior: smooth;
}
img { display: block; }
small { font-size: .35lh; }
strong { font-weight: 600; }
svg { display: block; }
figcaption, li, ol, p {
text-wrap: balance;
text-wrap: pretty;
}
/* ####### ##### ###### ## ## ###### */
/* ## ## ## ## ## ### ### ## */
/* ###### ## ## ###### ## # ## ##### */
/* ## ## ## ## ## ## ## ## */
/* ## ##### ## ## ## ## ###### */
form {
container: form / inline-size;
::placeholder { opacity: .65; }
fieldset {
form > & {
column-gap: clamp(var(--three-quarter-space), 3dvw, var(--space));
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: var(--one-and-three-quarter-space);
@container form (width < 400px) {
grid-template-columns: 1fr;
}
}
margin-top: -.5lh;
padding-top: .75lh;
legend {
font-weight: bolder;
position: relative;
top: .5lh;
}
}
.form-group {
position: relative;
transition: background .25s linear;
input, select, textarea {
border: 1px solid var(--color-01);
width: 100%;
&:not(:last-child, input[type="checkbox"], input[type="radio"]) {
margin-bottom: var(--half-space);
}
}
input[type="checkbox"],
input[type="radio"] {
aspect-ratio: 1;
background: var(--color-02);
height: .75lh;
width: .75lh;
&:checked { background: var(--color-01); }
.form-group:has(&) {
align-content: start;
display: flex;
flex-wrap: wrap;
gap: .5lh 2ch;
& > :where(label, legend) { flex: 1 1 100%; }
}
}
label {
&:not(:where(input[type="checkbox"], input[type="radio"]) + &,
&:has(+ :where([type="checkbox"], [type="radio"]))
) { font-weight: bolder; }
&:hover { cursor: pointer; }
}
textarea { height: 3lh; }
:disabled, label:has(:disabled, ~ :disabled), :disabled ~ label {
border-color: color-mix(in oklab, var(--color-01) 40%, transparent);
color: color-mix(in oklab, var(--color-01) 40%, transparent);
&:hover { cursor: not-allowed; }
}
/* wrapper for checkbox/radio input and label elements */
.form-group__item {
align-items: center;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
gap: .5ch;
& *:hover { cursor: pointer; }
}
.required__indicator { color: var(--danger-color); }
&.invalid {
background: color-mix(in oklab, var(--danger-color) 5%, transparent);
.invalid__message {
color: var(--danger-color);
font-size: 1.0rem;
inset: calc(100% + .5lh) auto auto 0;
position: absolute;
}
}
}
}

View File

@ -1,56 +0,0 @@
.align-center {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.align-left {
margin-left: 0;
margin-right: auto;
text-align: left;
}
.align-right {
margin-left: auto;
margin-right: 0;
text-align: right;
}
.keep { white-space: nowrap; }
/* https://fajarwz.com/blog/how-to-solve-unwanted-css-transitions-on-page-load/ */
.no-transition,
.no-transition * {
-webkit-transition: none !important;
-moz-transition: none !important;
-ms-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
@keyframes parallax-background {
to { background-position-y: -200px; }
}
.parallax-background {
position: relative;
overflow: clip;
background: none;
&::before {
animation: parallax-background linear;
animation-timeline: view();
background-image: var(--parallax-background); /* define --parallax-background in element's style attribute */
background-position-x: center;
background-repeat: no-repeat;
background-size: cover;
content: '';
height: calc(100% + 200px);
inset: 0;
position: absolute;
z-index: -1;
}
}

View File

@ -1,2 +0,0 @@
import('./validation/validation.js')
const save = () => alert('Congrats! You successfully filled out the form. Amazing.')

View File

@ -1,463 +0,0 @@
# Form Validation
Importing the `Validation` class queries the current page for `<form>` 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
<div class="form-group">
<label for="text">Text</label>
<input required id="text" type="text" name="text">
</div>
```
`<input>` `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 `<span>` 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 `<label>` element.
This indicator will be inserted to the `<legend>` element of `checkbox` and
`radio` `<input>` 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 `<label>` element.
- Unique, matching values of the required field's `id` and its `<label>`'s `for`
attribute.
- A unique `name` attribute on the required field. `checkbox` and `radio`
`<input>` 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
<div class="form-group">
<label for="text">Text</label>
<input id="text" type="text" name="text">
</div>
```
### Checkbox and Radio Fields
- Wrap all options in a `<fieldset>` element and add the `form-group` class to
it instead of the individual options.
- The `<legend>` tag may be used to give the option set a heading.
- Each `checkbox` or `radio` `<input>` type should be wrapped in a
`form-group__item` class. This class is not used programattically, and is only
used for styling.
```html
<fieldset class="form-group">
<legend>Check Group</legend>
<span class="form-group__item">
<input id="checkbox-1" type="checkbox" name="checks">
<label for="checkbox-1">Checkbox 1</label>
</span>
<span class="form-group__item">
<input id="checkbox-2" type="checkbox" name="checks">
<label for="checkbox-2">Checkbox 2</label>
</span>
</fieldset>
```
### Select Fields
Required `<select>` elements should contain an `<option>` with the `selected`
attribute and an empty `value` attribute. If not present the element will pass
validation even while being required.
```html
<div class="form-group">
<label for="select">Select</label>
<select id="select" name="select">
<option selected value="">Choose an Item</option>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
</div>
```
### Password Fields
When an `<input>` 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 `<input>` element,
and the `for` attribute of the `<label>` 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
<div class="form-group">
<label for="password">Password</label>
<input id="password" type="password" name="password">
</div>
```
will generate a *Password Confirm* field with the markup of
```html
<div class="form-group">
<label for="password-confirm">Password Confirm<span class="required__indicator">*</span></label>
<input id="password-confirm" type="password" name="password-confirm">
</div>
```
> The *Password* field is tested that it matches its corresponding
*Password Confirm* field when the *Password Confirm* field contains a non-blank
value.
<br><br>
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.<br>
`valueSrc[1]:` The keyword `attribute`, `attributeValue`, `exists`, or
`textContent`.<br>
`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
<div class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "exists", "img"]}' id="custom-control" name="custom_control"></div>
</div>
```
> 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
<div class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}' id="custom-control" name="custom_control" contenteditable></div>
</div>
```
### 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
<div class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "attribute", "title"]}' id="custom-control" name="custom_control"></div>
</div>
```
> 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
<div class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "attributeValue", "title"]}' id="custom-control" name="custom_control"></div>
</div>
```
> 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 `<form>` being validated. Its value must be
a properly formatted JSON object.
```html
<form data-validation='{"onlyOnSubmit": true, "reqIndicator": "❗️"}'>
<div class="form-group">
<label for="text">Text</label>
<input id="text" type="text" name="text">
</div>
<div class="form-group">
<button type="submit">Submit</button>
</div>
</form>
```
### invalidClass
**Default:** `invalid`
The class assigned to the parent `<div class="form-group" />` 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
<form data-validation='{"onSuccess": "save()"}'>...</form>
```
### 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
<form data-validation='{"passConfirm": true}'>...</form>
```
### reqAll
**Default:** `true`
Automatically add a `required` attribute to all `<input>`, `<select>`, and
`<textarea>` tags in the `<form>`. 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
<div class="form-group">
<label for="not-required">Not Required</label>
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
</div>
```
### 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 `<span>` 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
<div class="form-group">
<label for="custom">Custom Regex</label>
<input data-validation='{"optional": true,
"pattern": "^[0-9]*$",
"patternMessage": "This field must only contain numbers."
}'
id="custom" type="text" inputmode="numeric" name="custom">
</div>
```
### 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.

View File

@ -1,450 +0,0 @@
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',
`<span class="${this.validationInstance.reqIndicatorClass}">${this.validationInstance.reqIndicator}</span>`
)
}
}
// 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',
`<div class="${this.messageClass}">${error.message}</div>`
)
}
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}))
}
}
}
}

View File

@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form Validation</title>
<link rel="stylesheet" href="./css/main.css">
<script src="./js/main.js" defer></script>
<link rel="stylesheet" href="../../css/main.css">
<script src="./validation.js" defer></script>
</head>
<body class="no-transition">
@ -15,17 +15,7 @@
<div style="background: var(--color-02);">
<form data-validation='{"onSuccess": "save()"}'>
<fieldset>
<legend>Form Validation</legend>
<div class="form-group">
<label for="exists">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#exists", "exists", ":scope > img"]}' id="exists" name="exists"></div>
</div>
<figure class="form-group">
<label for="date">Date</label>
<input id="date" type="date" name="date" min="2025-01-01" max="2025-12-31">
</figure>
<legend>Native HTML Form Controls</legend>
<figure class="form-group">
<label for="text">Text</label>
@ -33,17 +23,6 @@
<figcaption>Input element with <code>type="text"</code> attribute.</figcaption>
</figure>
<figure class="form-group">
<label for="custom">Regex Pattern Test</label> <small>(Numbers only.)</small>
<input data-validation='{"pattern": "^[0-9]*$", "patternMessage": "This field must only contain numbers."}' id="custom" type="text" inputmode="numeric" name="custom">
<figcaption>This field uses a user supplied regular expression to test against.</figcaption>
</figure>
<figure class="form-group">
<label for="tel">Tel</label>
<input id="tel" type="tel" name="tel">
</figure>
<figure class="form-group">
<label for="email">Email</label>
<input id="email" type="email" name="email">
@ -51,31 +30,13 @@
</figure>
<figure class="form-group">
<label for="password">Password</label>
<input id="password" type="password" name="password">
<figcaption>Input element with <code>type="password"</code> attribute. The 'Password Confirm' field is auto-generated.</figcaption>
<label for="tel">Tel</label>
<input id="tel" type="tel" name="tel">
</figure>
<figure class="form-group">
<label for="not-required">Not Required</label>
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
<figcaption>Exclude fields.</figcaption>
</figure>
<figure class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}'
id="custom-control" name="custom_control" contenteditable
style="background: green;">
</div>
<figcaption>This field is a <code>div</code> element, not an <code>input</code> element.</figcaption>
</figure>
<figure class="form-group">
<label for="disabled">Dynamic Disabling</label>
<input id="disabled" type="text" name="disabled" disabled>
<button type="button" onclick="document.querySelector('#disabled').toggleAttribute('disabled')">Toggle Disabled Attribute</button>
<figcaption>Fields can be dynamically disabled/enabled.</figcaption>
<label for="date">Date</label>
<input id="date" type="date" name="date" min="2025-01-01" max="2025-12-31">
</figure>
<figure class="form-group">
@ -129,7 +90,70 @@
<label for="textarea">Textarea</label>
<textarea id="textarea" name="textarea"></textarea>
</figure>
</fieldset>
<fieldset>
<legend>Passwords</legend>
<p style="grid-column: -1 / 1;">The Password Confirm field below is automatically generated based on the presence of a Password field.</p>
<div class="form-group">
<label for="password">Password</label>
<input id="password" type="password" name="password">
</div>
</fieldset>
<fieldset>
<legend>Custom Form Controls</legend>
<figure class="form-group">
<label for="custom-control">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}'
id="custom-control" name="custom_control" contenteditable
style="background: green;">
</div>
<figcaption>This custom form control is a <code>div</code> element, not an <code>input</code> element.</figcaption>
</figure>
<figure class="form-group">
<label for="exists">Custom Form Control</label>
<div data-validation='{"valueSrc": ["#exists", "exists", ":scope > img"]}' id="exists" name="exists"></div>
<button type="button" onclick="document.querySelector('#exists').insertAdjacentHTML('afterbegin', '<img src=&quot;https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/heads/master/src/24/solid/hand-thumb-up.svg&quot; alt=&quot;Ya good.&quot; />')">Add Image</button>
<button type="button" onclick="document.querySelector('#exists').innerHTML = ''">Remove Image</button>
<figcaption>This custom form control checks if a the <code>div#exists</code> contains an image.</figcaption>
</figure>
</fieldset>
<fieldset>
<legend>Other Options</legend>
<p style="grid-column: -1 / 1;">These options can be applied to the fields above.</p>
<figure class="form-group">
<label for="not-required">Text (Not Required)</label>
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
<figcaption>Exclude fields.</figcaption>
</figure>
<figure class="form-group">
<label for="not-required-email">Email (Not Required)</label>
<input data-validation='{"optional": true}' id="not-required-email" type="email" name="not_required_email">
<figcaption>Exclude fields.</figcaption>
</figure>
<figure class="form-group">
<label for="custom">Regex Pattern Test</label> <small>(Numbers only.)</small>
<input data-validation='{"pattern": "^[0-9]*$", "patternMessage": "This field must only contain numbers."}' id="custom" type="text" inputmode="numeric" name="custom">
<figcaption>This field uses a user supplied regular expression to test against and can output a custom error message.</figcaption>
</figure>
<figure class="form-group">
<label for="disabled">Dynamic Disabling</label>
<input id="disabled" type="text" name="disabled" disabled>
<button type="button" onclick="document.querySelector('#disabled').toggleAttribute('disabled')">Toggle Disabled Attribute</button>
<figcaption>Required fields can be dynamically disabled/enabled.</figcaption>
</figure>
</fieldset>
<fieldset>
<figure class="form-group">
<button type="submit">Submit</button>
<button type="reset">Reset</button>

View File

@ -263,41 +263,41 @@ class Validation {
parseField(fieldInstance) {
const nameAttrVal = fieldInstance.el.name
if (!fieldInstance.optional) {
if (!fieldInstance.el.value) {
this.validateForBlank(fieldInstance)
this.validateForBlank(fieldInstance)
if (fieldInstance.el.value) {
if (fieldInstance.pattern) {
this.validatePattern(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
}
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.`
)
if (!fieldInstance.el.value && !fieldInstance.optional) {
this.addError(
fieldInstance,
(fieldInstance.labelText ? fieldInstance.labelText : `Field`) + ` is required.`
)
}
}
validatePattern(fieldInstance) {