Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active December 20, 2023 00:14
Show Gist options
  • Save dfkaye/4185cb43bd887bddb75c20d6c605fdab to your computer and use it in GitHub Desktop.
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
// 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