<div class="container">
<div class="col-container">
<h1>Longwinded Custom Validations</h1>
<p>... with Constraint Validation and <code>ElementInternals</code> API
<p>For demonstration's sake: a totally contrived example of an input expecting "Hello"</p>
<label for="validation-select">Validation Type</label>
<select id="validation-select" name="validation-type">
<option value="live" selected>Live (immediate)</option>
<option value="after">On submission</option>
<option value="blur">Late validation (onblur)</option>
<option selected value="none">None (bypass validation)</option>
</select>
<output class="output">
</output>
<fieldset>
<legend>Legend</legend>
<li>Default, on-submit validation: "Please fill out this field."</li>
<li>Live validation: "You must say hello!"</li>
<li> Late validation (after leaving field): "You didn't enter hello after leaving the field!"</legend>
</fieldset>
<form id="example-form" method="post" onsubmit="handleSubmit(event)">
<div class="fields">
<div class="field">
<label for="a">Greeting<span data-matches="required">*</span></label>
<input id="a" required aria-required name="a" pattern="hel{l,2}o" aria-describedby="input-required" placeholder="Say 'hello' " aria-placeholder=" Enter hello" minlength="5" maxlength="5" />
<div id="input-required" class="message">
<ul class="validation" role="list">
<li data-matches="valid">Currently <code>:valid</code></li>
<li data-matches="aria-invalid">
Currently <code>:aria-invalid</code>
</li>
<li data-matches="invalid">Currently <code>:invalid</code></li>
<li data-matches="user-valid">Currently <code>:user-valid</code></li>
<li data-matches="user-invalid">Currently <code>:user-invalid</code></li>
</ul>
</div>
</div>
<div class="field">
<label for="b">Control field <span data-matches="optional">(optional)</span></label>
<input id="b" name="b" aria-describedby="field-optional" />
<ul class="validation" role="list">
<li data-matches="valid">Currently <code>:valid</code></li>
<li data-matches="aria-invalid">
Currently <code>:aria-invalid</code>
</li>
<li data-matches="invalid">Currently <code>:invalid</code></li>
<li data-matches="user-valid">Currently <code>:user-valid</code></li>
<li data-matches="user-invalid">Currently <code>:user-invalid</code></li>
</ul>
</div>
</div>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</div>
</div>
</div>
</form>
</div>
</div>
/* Original HTML/CSS from https://codepen.io/web-dot-dev/pen/wvNJGrO
See https://web.dev/articles/user-valid-and-user-invalid-pseudo-classes */
input:user-valid {
--state-color: lightgreen;
}
input:valid {
--state-color: green;
}
input:invalid {
--state-color: red;
}
input[aria-invalid="true"],
input::aria-invalid {
--state-color: orange;
}
[aria-invalid="true"] ~ .errormessage,
[aria-errormessage] {
visibility: visible;
}
input:user-invalid {
--state-color: pink;
}
label + input:required {
content: "*";
}
label::after + input:optional {
content: " (optional)";
}
.container {
display: flex;
flex-direction: row;
}
.col-container {
display: flex;
flex-direction: column;
}
output {
margin-top: 2rem;
margin-left: 0.25rem;
height: 100%;
border: 1px solid gray;
border-radius: 4px;
word-wrap: break-word;
}
fieldset {
margin-top: 1rem;
margin-bottom: 1rem;
}
form {
margin: unset;
}
.fields {
display: flex;
flex-direction: row;
gap: 1rem;
}
.field {
display: flex;
flex-flow: column nowrap;
margin-bottom: 0.5em;
}
const form = document.getElementById("example-form");
const requiredTextField = document.getElementById("a");
const output = document.querySelector("output");
const setValidation = (msg) => {
if (!msg) requiredTextField.setCustomValidity("");
else {
requiredTextField.setCustomValidity(msg);
requiredTextField.validationMessage = msg;
}
};
function setErrorIfInvalid(id, searchTerm, msg) {
const element = document.getElementById(id);
if (!element.value && element.required) {
setValidation(msg);
}
if (element.value.includes(searchTerm)) {
setValidation();
}
}
const handleSubmit = (event) => {
event.preventDefault();
const { a } = form.elements;
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(
{
isFormValid: form.checkValidity(),
a: {
value: a.value,
validationMessage: a.validationMessage,
// willValidate when form is submitted
willValidate: a.willValidate
},
b: b.value
},
null,
2
);
output.appendChild(pre);
};
const removeEventListening = () => {
requiredTextField.removeEventListener("blur", setErrorIfInvalid, true);
requiredTextField.removeEventListener("input", setErrorIfInvalid, true);
};
const removeAllValidation = () => {
removeEventListening();
requiredTextField.classList.remove("invalid");
requiredTextField.classList.remove("valid");
requiredTextField.classList.remove("user-invalid");
requiredTextField.setCustomValidity("");
requiredTextField.removeAttribute("invalid");
optionalTextField.removeAttribute("valid");
requiredTextField.removeAttribute("ariaInvalid");
};
document.getElementById("validation-select").addEventListener("change", (e) => {
const selectedValue = e.target.value;
switch (selectedValue) {
case "after":
setValidation("");
removeAllValidation();
break;
case "live":
setValidation("");
requiredTextField.addEventListener("input", () => {
setErrorIfInvalid("a", "hello", "You must say hello");
requiredTextField.reportValidity();
});
break;
case "blur":
requiredTextField.addEventListener("blur", (e) => {
setErrorIfInvalid(
"a",
"hello",
"You didn't enter 'hello' after leaving the field!"
);
requiredTextField.reportValidity();
});
break;
case "none":
requiredTextField.setAttribute("formNoValidate", true);
form.setAttribute("noValidate", true);
removeAllValidation();
break;
default:
break;
}
});
This Pen doesn't use any external JavaScript resources.