As there’s trouble happening here, I’m going to use the following example radio buttons which must be checked along with the GDPR checkbox before the submit button is enabled.
Sample HTML code
Here is some sample HTML code for which we require that one of the options and the GDPR checkbox are both checked.
<form method="post" action="assign_gender.php">
<fieldset>
<legend>Gender identity</legend>
<p><input type="radio" name="gender" id="cisgender"> <label for="cisgender">Cisgender (matches sex)</label></p>
<p><input type="radio" name="gender" id="lesbian"> <label for="lesbian">Lesbian</label></p>
<p><input type="radio" name="gender" id="gay"> <label for="gay">Gay</label></p>
<p><input type="radio" name="gender" id="bisexual"> <label for="bisexual">Bisexual</label></p>
<p><input type="radio" name="gender" id="transgender"> <label for="transgender">Transgender</label></p>
<p><input type="radio" name="gender" id="transsexual"> <label for="transsexual">Transsexual</label></p>
<p><input type="radio" name="gender" id="queer"> <label for="queer">Queer</label></p>
<p><input type="radio" name="gender" id="questioning"> <label for="questioning">Questioning</label></p>
<p><input type="radio" name="gender" id="intersex"> <label for="intersex">Intersex</label></p>
<p><input type="radio" name="gender" id="asexual"> <label for="asexual">Asexual</label></p>
<p><input type="radio" name="gender" id="ally"> <label for="ally">Ally</label></p>
<p><input type="radio" name="gender" id="pansexual"> <label for="pansexual">Pansexual</label></p>
</fieldset>
<p><input type="checkbox" name="gdpr_consent" id="gdpr_consent"> <label for="gdpr_consent">GDPR consent?</label></p>
<p><input type="submit" value="Submit" disabled></p>
</form>
Initial JS code
Here is the working code that has been slightly modified to work both with checkboxes and options. It ensures that a radio selection and the GDPR checkbox are checked before the form can be submitted.
const radios = document.querySelectorAll('input[type="radio"]');
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
const button = document.querySelector('input[type="submit"]');
function countChecked(elements) {
return elements.filter(element => element.checked).length;
}
function handleInputChange(checkbox) {
button.setAttribute("disabled", "");
if (countChecked(radios) + countChecked(checkboxes) >= 2) {
button.removeAttribute('disabled');
}
}
radios.forEach(el => el.addEventListener('change', handleInputChange));
checkboxes.forEach(el => el.addEventListener('change', handleInputChange));
Checking the form on page load
Instead of setting the HTML element to be disabled, we can instead trigger the change event on one of the elements. That way on page load the code checks and finds we don’t have enough checked elements, and disables the submit button.
function triggerEvent(element, eventType) {
element.dispatchEvent(new Event(eventType));
}
...
triggerEvent(checkboxes[0], "change");
The disabled attribute is no longer needed on the submit button.
<!--<p><input type="submit" value="Submit" disabled></p>-->
<p><input type="submit" value="Submit"></p>
This lets the form be more accessible, so that it still works when JS isn’t active too.
Investigating the count check
The magic line that does the check is as follows:
if (countChecked(radios) + countChecked(checkboxes) >= 2) {
I don’t like how we are checking that the count matches a certain number. That results in too much fragility when things need to change.
There are certain things that can be done to better organize this check.
A list of named elements
Instead of checking for a certain number, it is better to have a list of named elements that we care about. That way we can check if there are the same number of named and checked elements.
Here is the list of names for the named elements:
// const radios = document.querySelectorAll('input[type="radio"]');
// const checkboxes = document.querySelectorAll('input[type="checkbox"]');
const names = ["gender", "gdpr_consent"];
const button = document.querySelector('input[type="submit"]');
The handleInputChange function can then call countChecked with elements retrieved from those names.
function handleInputChange(field) {
const elements = getElementsByNames(names);
button.setAttribute("disabled", "");
if (countChecked(elements) === names.length) {
button.removeAttribute('disabled');
}
}
We just need a getElementsByNames function to turn those names into matching elements:
function getElementsByNames(names) {
const getElements = selector => Array.from(document.querySelectorAll(selector));
return names.flatMap(name => getElements(`[name="${name}"]`));
}
I’m using a separate getElements function here to help us change the nodeList from querySelectorAll into an Array list. That way flatMap can easily turn the multiple arrays into a single array of elements instead.
Replacing checkboxes with named elements
The rest of the code that deals with checkboxes can be updated to use namedElements instead.
const namedElements = getElementsByNames(names);
// radios.forEach(el => el.addEventListener('change', handleInputChange));
// checkboxes.forEach(el => el.addEventListener('change', handleInputChange));
namedElements.forEach(el => el.addEventListener('change', handleInputChange));
// triggerEvent(checkboxes[0], "change");
triggerEvent(namedElements[0], "change");
Summary
Now, it is only the following two lines at the top of the code that are responsible for most of the configuration.
const names = ["gender", "gdpr_consent"];
const button = document.querySelector('input[type="submit"]');
and no matter how many named elements that we care about, the code ensures that all of the names must have a checked element.
Here is the full JS code:
const names = ["gender", "gdpr_consent"];
const button = document.querySelector('input[type="submit"]');
function getElementsByNames(names) {
const getElements = selector => Array.from(document.querySelectorAll(selector));
return names.flatMap(name => getElements(`[name="${name}"]`));
}
function countChecked(elements) {
return elements.filter(element => element.checked).length;
}
function handleInputChange(field) {
const elements = getElementsByNames(names);
button.setAttribute("disabled", "");
if (countChecked(elements) === names.length) {
button.removeAttribute('disabled');
}
}
function triggerEvent(element, eventType) {
element.dispatchEvent(new Event(eventType));
}
const namedElements = getElementsByNames(names);
namedElements.forEach(el => el.addEventListener('change', handleInputChange));
triggerEvent(namedElements[0], "change");
The full code with the above improvements is found at https://jsfiddle.net/u7j1hd36/2/
There’s only one thing extra needed and that’s a disabled message stating what needs to be done, for which some separate validation might be put to good use.