Skip to content

Commit 48a8398

Browse files
committed
feat(pat-validation): Support definition of minimum or maximum number of selections.
1 parent 7d0e6c2 commit 48a8398

File tree

4 files changed

+422
-5
lines changed

4 files changed

+422
-5
lines changed

src/pat/validation/documentation.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ These extra validation rules are:
2020

2121
- Equality checking between two fields (e.g. password confirmation).
2222
- Date and datetime validation for before and after a given date or another input field.
23+
- Minimum and maximum number of checked, selected or filled-out fields. Most useful for checkboxes, but also works for text-inputs, selects and other form elements.
2324

2425

2526
### HTML form validation framework integration.
@@ -146,7 +147,11 @@ More information on the `form` attribute can be found at [MDN](https://developer
146147
| message-number | The error message for numbers. | This value must be a number. | String |
147148
| message-required | The error message for required fields. | This field is required. | String |
148149
| message-equality | The error message for fields required to be equal | is not equal to %{attribute} | String |
150+
| message-min-values | The error message when the minimim number of checked, selected or filled-out fields has not been reached. | You need to select at least %{count} item(s). | String |
151+
| message-max-values | The error message when the maximum number of checked, selected or filled-out fields has not been reached. | You need to select at most %{count} item(s). | String |
149152
| equality | Field-specific extra rule. The name of another input this input should equal to (useful for password confirmation). | | String |
150153
| not-after | Field-specific extra rule. A lower time limit restriction for date and datetime fields. | | CSS Selector or a ISO8601 date string. |
151154
| not-before | Field-specific extra rule. An upper time limit restriction for date and datetime fields. | | CSS Selector or a ISO8601 date string. |
155+
| min-values | Minimum number of checked, selected or filled out form elements. | null | Integer (or null) |
156+
| max-values | Maximum number of checked, selected or filled out form elements. | null | Integer (or null) |
152157
| delay | Time in milliseconds before validation starts to avoid validating while typing. | 100 | Integer |

src/pat/validation/index.html

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,6 @@
115115
yellow</label>
116116
</fieldset>
117117

118-
<hr />
119-
120118
<label>Planning start
121119
<input class="pat-date-picker"
122120
id="planning-start"
@@ -134,8 +132,7 @@
134132
autocomplete="off"
135133
name="measure.planning_end:records"
136134
type="date"
137-
value="2015-09-10"
138-
data-pat-date-picker="after: #planning-start"
135+
value="2015-09-09"
139136
data-pat-validation="not-before: #planning-start; message-date: This date must be on or after the start date."
140137
/>
141138
</label>
@@ -250,6 +247,154 @@
250247
</fieldset>
251248
</form>
252249

250+
<h2>Demo with max-values / min-values support</h2>
251+
<form class="pat-validation pat-checklist"
252+
action="."
253+
method="post"
254+
>
255+
<fieldset>
256+
<legend>Multi select</legend>
257+
<select
258+
name="select"
259+
multiple
260+
required
261+
data-pat-validation="
262+
min-values: 2;
263+
max-values: 3;
264+
"
265+
>
266+
<option value="a">a</option>
267+
<option value="b">b</option>
268+
<option value="c">c</option>
269+
<option value="d">d</option>
270+
</select>
271+
</fieldset>
272+
273+
<fieldset>
274+
<legend>Multiple checkboxes</legend>
275+
<label>
276+
a
277+
<input
278+
type="checkbox"
279+
name="checkbox[]"
280+
value="a"
281+
data-pat-validation="
282+
min-values: 1;
283+
max-values: 3;
284+
"
285+
/>
286+
</label>
287+
<label>
288+
b
289+
<input
290+
type="checkbox"
291+
name="checkbox[]"
292+
value="b"
293+
data-pat-validation="
294+
min-values: 1;
295+
max-values: 3;
296+
"
297+
/>
298+
</label>
299+
<label>
300+
c
301+
<input
302+
type="checkbox"
303+
name="checkbox[]"
304+
value="c"
305+
data-pat-validation="
306+
min-values: 1;
307+
max-values: 3;
308+
"
309+
/>
310+
</label>
311+
<label>
312+
d
313+
<input
314+
type="checkbox"
315+
name="checkbox[]"
316+
value="d"
317+
data-pat-validation="
318+
min-values: 1;
319+
max-values: 3;
320+
"
321+
/>
322+
</label>
323+
</fieldset>
324+
325+
<fieldset
326+
data-pat-validation="
327+
min-values: 2;
328+
max-values: 3;
329+
"
330+
>
331+
<legend>Demo with mixed inputs and max/min values support.</legend>
332+
<fieldset>
333+
<select
334+
name="multiple"
335+
multiple
336+
>
337+
<option value="a">a</option>
338+
<option value="b">b</option>
339+
<option value="c">c</option>
340+
<option value="d">d</option>
341+
</select>
342+
</fieldset>
343+
<fieldset>
344+
<label>
345+
a
346+
<input
347+
type="checkbox"
348+
name="multiple"
349+
value="a"
350+
/>
351+
</label>
352+
<label>
353+
b
354+
<input
355+
type="checkbox"
356+
name="multiple"
357+
value="b"
358+
/>
359+
</label>
360+
<label>
361+
c
362+
<input
363+
type="checkbox"
364+
name="multiple"
365+
value="c"
366+
/>
367+
</label>
368+
<label>
369+
d
370+
<input
371+
type="checkbox"
372+
name="multiple"
373+
value="d"
374+
/>
375+
</label>
376+
</fieldset>
377+
<fieldset>
378+
<label>
379+
input 1
380+
<input name="multiple"/>
381+
</label>
382+
<label>
383+
input 2
384+
<input name="multiple"/>
385+
</label>
386+
<label>
387+
input 3
388+
<input name="multiple"/>
389+
</label>
390+
</fieldset>
391+
</fieldset>
392+
<fieldset class="buttons">
393+
<button>Submit</button>
394+
<button formnovalidate>Cancel</button>
395+
</fieldset>
396+
</form>
397+
253398
<div class="pat-modal">
254399
<form class="pat-inject vertical pat-validation"
255400
action="."

src/pat/validation/validation.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ parser.addArgument("message-min", ""); // "This value must be greater than or eq
2222
parser.addArgument("message-number", ""); // "This value must be a number"
2323
parser.addArgument("message-required", ""); // "This field is required"
2424
parser.addArgument("message-equality", "is not equal to %{attribute}.");
25+
parser.addArgument("message-min-values", "You need to select at least %{count} item(s).");
26+
parser.addArgument("message-max-values", "You need to select at most %{count} item(s).");
2527
parser.addArgument("not-after", null);
2628
parser.addArgument("not-before", null);
2729
parser.addArgument("equality", null);
30+
parser.addArgument("min-values", null);
31+
parser.addArgument("max-values", null);
2832
parser.addArgument("delay", 100); // Delay before validation is done to avoid validating while typing.
2933

3034
// Aliases
@@ -260,6 +264,56 @@ class Pattern extends BasePattern {
260264
logger.debug("Check `no-before` input.", not_after_el);
261265
this.check_input({ input: not_before_el, stop: true });
262266
}
267+
} else if (input_options.minValues || input_options.maxValues) {
268+
const min_values = input_options.minValues !== null && parseInt(input_options.minValues, 10) || null;
269+
const max_values = input_options.maxValues !== null && parseInt(input_options.maxValues, 10) || null;
270+
271+
let number_values = 0;
272+
for (const _inp of this.form.elements) {
273+
// Filter for siblings with same name.
274+
if (
275+
// Keep only inputs with same name
276+
_inp.name !== input.name
277+
// Skip all form elements which are no input elements
278+
|| ! ["INPUT", "SELECT", "TEXTAREA"].includes(_inp.tagName)
279+
) {
280+
continue;
281+
}
282+
283+
// Check if checkboxes or radios are checked ...
284+
if (_inp.type === "checkbox" || _inp.type === "radio") {
285+
if (_inp.checked) {
286+
number_values++;
287+
}
288+
continue;
289+
}
290+
291+
// Select, if select is selected.
292+
if (_inp.tagName === "SELECT") {
293+
number_values += _inp.selectedOptions.length;
294+
continue;
295+
}
296+
297+
// For the rest a value must be set.
298+
if (_inp.value === 0 || _inp.value) {
299+
number_values++;
300+
}
301+
}
302+
303+
if (max_values !== null && number_values > max_values) {
304+
this.set_error({
305+
input: input,
306+
msg: input_options.message["max-values"],
307+
max: max_values,
308+
})
309+
}
310+
if (min_values !== null && number_values < min_values) {
311+
this.set_error({
312+
input: input,
313+
msg: input_options.message["min-values"],
314+
min: min_values,
315+
})
316+
}
263317
}
264318

265319
if (!validity_state.customError) {
@@ -350,7 +404,11 @@ class Pattern extends BasePattern {
350404
}
351405
msg = msg.replace(/%{value}/g, JSON.stringify(input.value));
352406

353-
input.setCustomValidity(msg);
407+
// Set the error state the input itself and on all siblings, if any.
408+
const inputs = [...this.form.elements].filter((_input) => _input.name === input.name);
409+
for (const _input of inputs) {
410+
_input.setCustomValidity(msg);
411+
}
354412
// Store the error message on the input.
355413
// Hidden inputs do not participate in validation but we need this
356414
// (e.g. styled date input).

0 commit comments

Comments
 (0)