Last active
December 20, 2023 00:14
-
-
Save dfkaye/4185cb43bd887bddb75c20d6c605fdab to your computer and use it in GitHub Desktop.
demo example of native form validation in 2023 with dynamic cross-field checks and dynamic custom validity messages
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 19 October 2023 | |
// native form validation with dynamic cross-field checks and dynamic custom | |
// validity messages. | |
// Very "simple" demo can be run from the browser's console. Styles may be | |
// blocked if you're running a blank or incognito window or a site with a very | |
// strict Content Security Policy. | |
// First we'll set up a form to test against. | |
var html = ` | |
<style> | |
:invalid { outline: 1px solid red; } | |
:focus { background-color: aqua; } | |
</style> | |
<form name="native-form-validation-test"> | |
<fieldset> | |
<label for="lowercase">lowercase (optional)</label> | |
<input name="lowercase" pattern="[a-z]" id="lowercase"> | |
</fieldset> | |
<fieldset> | |
<label for="UPPERCASE">UPPERCASE (optional)</label> | |
<p> | |
<small> | |
This field is optional unless the <b>lowercase</b> field contains a | |
value. | |
</small> | |
</p> | |
<input name="UPPERCASE" pattern="[A-Z]" id="UPPERCASE"> | |
</fieldset> | |
<fieldset> | |
<label for="digits">number (optional)</label> | |
<input name="digits" type="number" min="1" max="2" id="digits"> | |
</fieldset> | |
<fieldset> | |
<label for="required-nws">required non-whitespace</label> | |
<input name="required-nws" required pattern="[\\S]+" id="required-nws"> | |
</fieldset> | |
<fieldset> | |
<legend>traffic radio group</legend> | |
<p> | |
<small> | |
You can't use pattern or the other attributes to validate radio groups | |
(or checkboxes). You have to select a radio as a default in the markup. | |
</small> | |
</p> | |
<input name="traffic" type="radio" value="red" id="red" checked> | |
<label for="red">red</label> | |
<input name="traffic" type="radio" value="yellow" id="yellow"> | |
<label for="yellow">yellow</label> | |
<input name="traffic" type="radio" value="green" id="green"> | |
<label for="green">green</label> | |
</fieldset> | |
<fieldset> | |
<label for="traffic-list">traffic-list</label> | |
<select name="traffic-list" id="traffic-list" required pattern="[\\S]+]"> | |
<option value="">Choose color...</option> | |
<option value="red" style="color: red;">red</option> | |
<option value="yellow" style="color: yellow;">yellow</option> | |
<option value="green" style="color: green;">green</option> | |
</select> | |
</fieldset> | |
<fieldset> | |
<label for="textarea">textarea (between 10 and 20 characters)</label> | |
<textarea name="textarea" id="textarea" required minlength="10" maxlength="20"></textarea> | |
</fieldset> | |
<button type="submit">Submit</button> | |
<button type="reset">Reset</button> | |
</form> | |
`; | |
var body = document.body; | |
typeof body.setHtml == "function" | |
? body.setHtml(html) // Use this if browser enforces Trusted HTML policy. | |
: body.innerHTML = html; | |
// Next, set up a list of messages for each validity error we care about. This | |
// is a single list for demo purposes. You may wish to customize it for each | |
// field to be validated. For internalization you'll need to generate different | |
// lists for each language or locale you want to support... | |
var messages = { | |
badInput: "bad input", | |
// customError: "custom error (not sure whether to use this)", | |
patternMismatch: "pattern mismatch", | |
rangeOverflow: "range overflow", | |
rangeUnderflow: "range underflow", | |
stepMismatch: "step mismatch", | |
tooLong: "too long", | |
tooShort: "too short", | |
typeMismatch: "type mismatch", | |
// valid: "don't use this", | |
valueMissing: "(required or optional) value missing" | |
} | |
// Now comes the `check()` function. | |
// | |
// This function is called multiple times, once over each element during form | |
// initialization, and when an element's value changes (via the 'input' event). | |
// | |
// There is no need to run it on all the elements when the form is submitted. | |
// | |
// The first parameter is the elements collection belonging to the form to allow | |
// for a crude "dependent" checking demo via the third parameter. | |
// | |
// The function checks the current validity of the element at the index | |
// specified, setting the next validity message to display if the element value | |
// is currently invalid. If the current value is valid, the validity message is | |
// emptied. | |
function check(elements, index, required) { | |
var element = elements[index || 0]; | |
// Introduced this check when I added fieldsets to contain each form element. | |
// Unfortunately fieldsets are included in the form.elements collection so we | |
// have to skip past them... | |
if (!/input|select|textarea/i.test(element.nodeName)) { | |
return; | |
} | |
var state = element.validity; | |
if (+required == +required) { | |
// If the required param is passed as a number (not NaN), set the current | |
// element's 'required' property to match its boolean value. This allows us | |
// to clear it if the "requiring" element has a valid but empty value, where | |
// "valid but empty" means a field is not "required" and its value is empty. | |
element.required = required; | |
} | |
for (var k in state) { | |
// Find the validity state key set to true (indicating a problem). We return | |
// early from this function if we find a matching error state. | |
// Note: The second condition checks that a message for this key exists, | |
// meaning that we care about which type of validation error this is. | |
if (state[k] && k in messages) { | |
element.setCustomValidity(messages[k]); | |
// Not necessary here, prefer CSS. Used in case of CSP block. | |
element.style.outline = "1px solid red"; | |
return false; | |
} | |
} | |
// Empty the message for the next validation check because the field is valid. | |
element.setCustomValidity(""); | |
// Again not necessary, prefer CSS. Used in case of CSP block. | |
element.style.outline = "initial"; | |
////////////////////////////////////////////////////////////////////////////// | |
// This next step is merely to demonstrate dynamic cross-field dependency. | |
// | |
// Send the current input's value length as the indicator that the next input | |
// is to be required if we're on the first input and it's valid and non-empty. | |
////////////////////////////////////////////////////////////////////////////// | |
if (index < 2) { | |
var next = elements[index += 1]; | |
// Except for the inclusion of fieldsets in the form.elements collections, | |
// that collection makes it convenient to jump to related fields that need | |
// to be required or not based on the current element's validity and value. | |
// The loop increments the index variable until we have a match. | |
while (next) { | |
if (/input|select|textarea/i.test(next.nodeName)) { | |
return check( elements, index, element.value.length ); | |
} | |
next = elements[index += 1]; | |
} | |
} | |
return true; | |
} | |
// Finally, when the DOM is loaded: | |
// 1. Initialize all elements and set up 'input' event handler for each. | |
Array.from(document.forms[0].elements).forEach(function (element, index) { | |
// Introduced this check when I added fieldsets to contain each form element. | |
// Unfortunately fieldsets are included in the form.elements collection so we | |
// have to skip past them... | |
if (!/input|select|textarea/i.test(element.nodeName)) { | |
return; | |
} | |
element.oninput = function (e) { | |
console.log(e.detail || e.target.id); | |
return check( element.form.elements, index ); | |
}; | |
// Either of these will work to initialize the element, use the one you're | |
// comfortable with. | |
check( element.form.elements, index ); | |
element.dispatchEvent(new CustomEvent("input", { | |
detail: `initializing element ${index}` | |
})); | |
////////////////////////////////////////////////////////////////////////////// | |
// These are unnecessary but I'm working against a strong Content Security | |
// Policy that prevents injecting style elements yadda yadda yadda... | |
element.style.backgroundColor = "white"; | |
element.style.color = "chocolate"; | |
element.onfocus = function (e) { e.target.style.backgroundColor = "aqua"; }; | |
element.onblur = function (e) { e.target.style.backgroundColor = "white"; }; | |
////////////////////////////////////////////////////////////////////////////// | |
}); | |
// 2. If there are validation issues on the form, the browser will not execute | |
// this method; if there are none, the browser will log data and show the | |
// alert. No need to re-check all elements with a loop, etc. | |
document.forms[0].onsubmit = function(e) { | |
console.info("submitting..."); | |
var data = new FormData(e.target); | |
for (var [k,v] of data.entries()) { | |
// Some styled console output... | |
console.log(`${k}: %c${v}`, "background-color: aqua; color: chocolate"); | |
} | |
e.preventDefault(); | |
alert("should submit now"); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment