Skip to content

Commit 86deccd

Browse files
authored
Merge pull request #11720 from SassNinja:fix/11683_abide_checkbox_group
feat: add required checkbox support to Abide
2 parents 379d670 + b541789 commit 86deccd

File tree

4 files changed

+283
-4
lines changed

4 files changed

+283
-4
lines changed

docs/pages/abide.md

+40
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,46 @@ When the Form Errors cannot be placed next to its field, like in an Input Group,
246246
</form>
247247
```
248248

249+
## Required Radio & Checkbox
250+
251+
If you add `required` to a radio or checkbox input the whole group gets considered as required. This means at least one of the inputs must be checked.
252+
Checkbox inputs support the additional attribute `data-min-required` what lets you specify how many checkboxes in the group must be checked (default is one).
253+
254+
255+
```html_example
256+
<form data-abide novalidate>
257+
<div class="grid-x grid-margin-x align-bottom">
258+
<div class="cell medium-6 large-4">
259+
<fieldset>
260+
<legend>Radio Group</legend>
261+
<input type="radio" name="exampleRadio" id="exampleRadioA" value="A">
262+
<label for="exampleRadioA">A</label>
263+
<input required type="radio" name="exampleRadio" id="exampleRadioB" value="B">
264+
<label for="exampleRadioB">B</label>
265+
<input type="radio" name="exampleRadio" id="exampleRadioC" value="C">
266+
<label for="exampleRadioC">C</label>
267+
</fieldset>
268+
</div>
269+
<div class="cell medium-6 large-4">
270+
<fieldset>
271+
<legend>Checkbox Group</legend>
272+
<input data-min-required="2" type="checkbox" name="exampleCheckbox" id="exampleCheckboxA" value="A">
273+
<label for="exampleCheckboxA">A</label>
274+
<input required type="checkbox" name="exampleCheckbox" id="exampleCheckboxB" value="B">
275+
<label for="exampleCheckboxB">B</label>
276+
<input type="checkbox" name="exampleCheckbox" id="exampleCheckboxC" value="C">
277+
<label for="exampleCheckboxC">C</label>
278+
</fieldset>
279+
</div>
280+
<div class="cell large-4">
281+
<button class="button" type="submit">Submit</button>
282+
</div>
283+
</div>
284+
</form>
285+
```
286+
287+
---
288+
249289
## Event Listener
250290
Setup event listener after foundation is initialized (especially for formvalid/forminvalid). Easier to chain via document selector.
251291
* valid.zf.abide and invalid.zf.abide are field level events, triggered in validateInput function

js/foundation.abide.js

+121-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class Abide extends Plugin {
134134
* @returns {Object} jQuery object with the selector.
135135
*/
136136
findFormError($el) {
137-
var id = $el[0].id;
137+
var id = $el.length ? $el[0].id : '';
138138
var $error = $el.siblings(this.options.formErrorSelector);
139139

140140
if (!$error.length) {
@@ -189,6 +189,28 @@ class Abide extends Plugin {
189189
return $(labels);
190190
}
191191

192+
/**
193+
* Get the set of labels associated with a set of checkbox els in this order
194+
* 2. The <label> with the attribute `[for="someInputId"]`
195+
* 3. The `.closest()` <label>
196+
*
197+
* @param {Object} $el - jQuery object to check for required attribute
198+
* @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
199+
*/
200+
findCheckboxLabels($els) {
201+
var labels = $els.map((i, el) => {
202+
var id = el.id;
203+
var $label = this.$element.find(`label[for="${id}"]`);
204+
205+
if (!$label.length) {
206+
$label = $(el).closest('label');
207+
}
208+
return $label[0];
209+
});
210+
211+
return $(labels);
212+
}
213+
192214
/**
193215
* Adds the CSS error class as specified by the Abide settings to the label, input, and the form
194216
* @param {Object} $el - jQuery object to add the class to
@@ -292,6 +314,31 @@ class Abide extends Plugin {
292314

293315
}
294316

317+
/**
318+
* Remove CSS error classes etc from an entire checkbox group
319+
* @param {String} groupName - A string that specifies the name of a checkbox group
320+
*
321+
*/
322+
removeCheckboxErrorClasses(groupName) {
323+
var $els = this.$element.find(`:checkbox[name="${groupName}"]`);
324+
var $labels = this.findCheckboxLabels($els);
325+
var $formErrors = this.findFormError($els);
326+
327+
if ($labels.length) {
328+
$labels.removeClass(this.options.labelErrorClass);
329+
}
330+
331+
if ($formErrors.length) {
332+
$formErrors.removeClass(this.options.formErrorClass);
333+
}
334+
335+
$els.removeClass(this.options.inputErrorClass).attr({
336+
'data-invalid': null,
337+
'aria-invalid': null
338+
});
339+
340+
}
341+
295342
/**
296343
* Removes CSS error class as specified by the Abide settings from the label, input, and the form
297344
* @param {Object} $el - jQuery object to remove the class from
@@ -301,6 +348,10 @@ class Abide extends Plugin {
301348
if($el[0].type == 'radio') {
302349
return this.removeRadioErrorClasses($el.attr('name'));
303350
}
351+
// checkboxes need to clear all of the els
352+
else if($el[0].type == 'checkbox') {
353+
return this.removeCheckboxErrorClasses($el.attr('name'));
354+
}
304355

305356
var $label = this.findLabel($el);
306357
var $formError = this.findFormError($el);
@@ -345,7 +396,8 @@ class Abide extends Plugin {
345396
break;
346397

347398
case 'checkbox':
348-
validated = clearRequire;
399+
validated = this.validateCheckbox($el.attr('name'));
400+
clearRequire = true;
349401
break;
350402

351403
case 'select':
@@ -405,8 +457,21 @@ class Abide extends Plugin {
405457
validateForm() {
406458
var acc = [];
407459
var _this = this;
460+
var checkboxGroupName;
461+
462+
// Remember first form submission to prevent specific checkbox validation (more than one required) until form got initially submitted
463+
if (!this.initialized) {
464+
this.initialized = true;
465+
}
408466

409467
this.$inputs.each(function() {
468+
469+
// Only use one checkbox per group since validateCheckbox() iterates over all associated checkboxes
470+
if ($(this)[0].type === 'checkbox') {
471+
if ($(this).attr('name') === checkboxGroupName) return true;
472+
checkboxGroupName = $(this).attr('name');
473+
}
474+
410475
acc.push(_this.validateInput($(this)));
411476
});
412477

@@ -495,6 +560,60 @@ class Abide extends Plugin {
495560
return valid;
496561
}
497562

563+
/**
564+
* Determines whether or a not a checkbox input is valid based on whether or not it is required and checked. Although the function targets a single `<input>`, it validates by checking the `required` and `checked` properties of all checkboxes in its group.
565+
* @param {String} groupName - A string that specifies the name of a checkbox group
566+
* @returns {Boolean} Boolean value depends on whether or not at least one checkbox input has been checked (if it's required)
567+
*/
568+
validateCheckbox(groupName) {
569+
// If at least one checkbox in the group has the `required` attribute, the group is considered required
570+
// Per W3C spec, all checkboxes in a group should have `required`, but we're being nice
571+
var $group = this.$element.find(`:checkbox[name="${groupName}"]`);
572+
var valid = false, required = false, minRequired = 1, checked = 0;
573+
574+
// For the group to be required, at least one checkbox needs to be required
575+
$group.each((i, e) => {
576+
if ($(e).attr('required')) {
577+
required = true;
578+
}
579+
});
580+
if(!required) valid=true;
581+
582+
if (!valid) {
583+
// Count checked checkboxes within the group
584+
// Use data-min-required if available (default: 1)
585+
$group.each((i, e) => {
586+
if ($(e).prop('checked')) {
587+
checked++;
588+
}
589+
if (typeof $(e).attr('data-min-required') !== 'undefined') {
590+
minRequired = parseInt($(e).attr('data-min-required'));
591+
}
592+
});
593+
594+
// For the group to be valid, the minRequired amount of checkboxes have to be checked
595+
if (checked >= minRequired) {
596+
valid = true;
597+
}
598+
};
599+
600+
// Skip validation if more than 1 checkbox have to be checked AND if the form hasn't got submitted yet (otherwise it will already show an error during the first fill in)
601+
if (this.initialized !== true && minRequired > 1) {
602+
return true;
603+
}
604+
605+
// Refresh error class for all input
606+
$group.each((i, e) => {
607+
if (!valid) {
608+
this.addErrorClasses($(e));
609+
} else {
610+
this.removeErrorClasses($(e));
611+
}
612+
});
613+
614+
return valid;
615+
}
616+
498617
/**
499618
* Determines if a selected input passes a custom validation function. Multiple validations can be used, if passed to the element with `data-validator="foo bar baz"` in a space separated listed.
500619
* @param {Object} $el - jQuery input element.

test/javascript/components/abide.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ describe('Abide', function() {
5252
});
5353

5454
it('returns true for checked checkboxes', function() {
55-
$html = $("<form data-abide><input type='checkbox' required checked='checked'></form>").appendTo("body");
55+
$html = $("<form data-abide novalidate><input type='checkbox' name='groupName' required checked='checked'></form>").appendTo("body");
5656
plugin = new Foundation.Abide($html, {});
5757

5858
plugin.validateInput($html.find("input")).should.equal(true);
5959
});
6060

6161
it('returns false for unchecked checkboxes', function() {
62-
$html = $("<form data-abide><input type='checkbox' required></form>").appendTo("body");
62+
$html = $("<form data-abide novalidate><input type='checkbox' name='groupName' required></form>").appendTo("body");
6363
plugin = new Foundation.Abide($html, {});
6464

6565
plugin.validateInput($html.find("input")).should.equal(false);
@@ -163,6 +163,19 @@ describe('Abide', function() {
163163
});
164164
});
165165

166+
describe('removeCheckboxErrorClasses()', function() {
167+
it('removes aria-invalid attribute from checkbox group', function() {
168+
$html = $('<form data-abide><input type="checkbox" name="groupName"></form>').appendTo('body');
169+
plugin = new Foundation.Abide($html, {});
170+
// Add error classes first
171+
plugin.addErrorClasses($html.find('input'));
172+
173+
plugin.removeCheckboxErrorClasses('groupName');
174+
175+
$html.find('input').should.not.have.attr('aria-invalid')
176+
});
177+
});
178+
166179
describe('resetForm()', function() {
167180
it('removes aria-invalid attribute from elements', function() {
168181
$html = $('<form data-abide><input type="text"></form>').appendTo('body');
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<!doctype html>
2+
<!--[if IE 9]><html class="lt-ie10" lang="en" > <![endif]-->
3+
<html class="no-js" lang="en" dir="ltr">
4+
<head>
5+
<meta charset="utf-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<title>Foundation for Sites Testing</title>
8+
<link href="../assets/css/foundation.css" rel="stylesheet" />
9+
</head>
10+
<body>
11+
<div class="grid-container">
12+
<div class="grid-x grid-padding-x">
13+
<div class="cell">
14+
<h1>Abide Checkboxes</h1>
15+
16+
<p>This form has required checkboxes. If you try to submit without picking one, it
17+
should show an error. When you then pick one, the error should clear and let you submit.</p>
18+
<form id="form" data-abide novalidate>
19+
<div class="alert callout hide" data-abide-error>
20+
<p>This form has errors.</p>
21+
</div>
22+
<fieldset>
23+
<legend>Fieldset Label</legend>
24+
<input required type="checkbox" name="example1" value="yes" id="example1Yes" />
25+
<label for="example1Yes">Yes</label>
26+
<input required type="checkbox" name="example1" value="no" id="example1No" />
27+
<label for="example1No">No</label>
28+
</fieldset>
29+
<button class="button" type="submit">Submit</button>
30+
<button class="button" type="reset">Reset</button>
31+
</form>
32+
33+
<hr>
34+
35+
<p>This form has <strong>one</strong> required checkbox. If you try to submit without picking one, it
36+
should show an error. When you then pick one, the error should clear and let you submit.</p>
37+
<form id="form" data-abide novalidate>
38+
<div class="alert callout hide" data-abide-error>
39+
<p>This form has errors.</p>
40+
</div>
41+
<fieldset>
42+
<legend>Fieldset Label</legend>
43+
<input type="checkbox" name="example3" value="yes" id="example3Yes" />
44+
<label for="example3Yes">Yes</label>
45+
<input required type="checkbox" name="example3" value="no" id="example3No" />
46+
<label for="example3No">No</label>
47+
<input type="checkbox" name="example3" value="maybe" id="example3Maybe" />
48+
<label for="example3Maybe">Maybe</label>
49+
</fieldset>
50+
<button class="button" type="submit">Submit</button>
51+
<button class="button" type="reset">Reset</button>
52+
</form>
53+
54+
<hr>
55+
56+
<p>This form has optional checkboxes. It should let you submit with or without picking one.</p>
57+
<form id="form" data-abide novalidate>
58+
<div class="alert callout hide" data-abide-error>
59+
<p>This form has errors.</p>
60+
</div>
61+
<fieldset>
62+
<legend>Fieldset Label</legend>
63+
<input type="checkbox" name="example2" value="yes" id="example2Yes" />
64+
<label for="example2Yes">Yes</label>
65+
<input type="checkbox" name="example2" value="no" id="example2No" />
66+
<label for="example2No">No</label>
67+
</fieldset>
68+
<button class="button" type="submit">Submit</button>
69+
<button class="button" type="reset">Reset</button>
70+
</form>
71+
72+
<hr>
73+
74+
<p>This form has a required checkbox with a custom value for <strong>data-min-required</strong> that specifies how many checkboxes must be checked in the group. If you try to submit without checking at least 3 checkboxes, it
75+
should show an error. When you then pick the required amount, the error should clear and let you submit.</p>
76+
<form id="form" data-abide novalidate>
77+
<div class="alert callout hide" data-abide-error>
78+
<p>This form has errors.</p>
79+
</div>
80+
<fieldset>
81+
<legend>Fieldset Label</legend>
82+
<input type="checkbox" name="example4" value="yes" id="example4Yes" />
83+
<label for="example4Yes">Yes</label>
84+
<input required data-min-required="3" type="checkbox" name="example4" value="no" id="example4No" />
85+
<label for="example4No">No</label>
86+
<input type="checkbox" name="example4" value="maybe" id="example4Maybe" />
87+
<label for="example4Maybe">Maybe</label>
88+
<input type="checkbox" name="example4" value="some" id="example4Some" />
89+
<label for="example4Some">Some</label>
90+
<input type="checkbox" name="example4" value="more" id="example4More" />
91+
<label for="example4More">More</label>
92+
</fieldset>
93+
<button class="button" type="submit">Submit</button>
94+
<button class="button" type="reset">Reset</button>
95+
</form>
96+
97+
<hr>
98+
</div>
99+
</div>
100+
</div>
101+
102+
<script src="../assets/js/vendor.js"></script>
103+
<script src="../assets/js/foundation.js"></script>
104+
<script>
105+
$(document).foundation();
106+
</script>
107+
</body>

0 commit comments

Comments
 (0)