+ `;
+
+ const outside = document.querySelector("input[name=outside]");
+ const inside = document.querySelector("input[name=inside]");
+ const form = document.querySelector("form");
+
+ expect(outside.form).toBe(form);
+ expect(inside.form).toBe(form);
+ });
+ });
+
describe("document_ready", () => {
it("calls the callback, once the document is ready.", async () => {
let cnt = 0;
diff --git a/src/core/events.js b/src/core/events.js
index 724c43546..0c481f962 100644
--- a/src/core/events.js
+++ b/src/core/events.js
@@ -172,7 +172,7 @@ const await_pattern_init = (pattern) => {
* A event factory for a bubbling and cancelable generic event.
*
* @param {string} name - The event name.
- * @returns {Event} - Returns a blur event.
+ * @returns {Event} - Returns a DOM event.
*/
const generic_event = (name) => {
return new Event(name, {
@@ -231,6 +231,20 @@ const focus_event = () => {
});
};
+const focusin_event = () => {
+ return new Event("focusin", {
+ bubbles: true,
+ cancelable: false,
+ });
+};
+
+const focusout_event = () => {
+ return new Event("focusout", {
+ bubbles: true,
+ cancelable: false,
+ });
+};
+
const input_event = () => {
return new Event("input", {
bubbles: true,
@@ -293,6 +307,8 @@ export default {
click_event: click_event,
change_event: change_event,
focus_event: focus_event,
+ focusin_event: focusin_event,
+ focusout_event: focusout_event,
input_event: input_event,
mousedown_event: mousedown_event,
mouseup_event: mouseup_event,
diff --git a/src/core/events.test.js b/src/core/events.test.js
index 42f03b98a..9be828bd0 100644
--- a/src/core/events.test.js
+++ b/src/core/events.test.js
@@ -529,6 +529,24 @@ describe("core.events tests", () => {
expect(catched).toBe("inner");
});
+ it("focusin event", async () => {
+ outer.addEventListener("focusin", () => {
+ catched = "outer";
+ });
+ inner.dispatchEvent(events.focusin_event());
+ await utils.timeout(1);
+ expect(catched).toBe("outer");
+ });
+
+ it("focusout event", async () => {
+ outer.addEventListener("focusout", () => {
+ catched = "outer";
+ });
+ inner.dispatchEvent(events.focusout_event());
+ await utils.timeout(1);
+ expect(catched).toBe("outer");
+ });
+
it("input event", async () => {
outer.addEventListener("input", () => {
catched = "outer";
diff --git a/src/pat/auto-suggest/auto-suggest.js b/src/pat/auto-suggest/auto-suggest.js
index a322dcb70..1a484cea5 100644
--- a/src/pat/auto-suggest/auto-suggest.js
+++ b/src/pat/auto-suggest/auto-suggest.js
@@ -122,9 +122,9 @@ export default Base.extend({
const val = $sel2.select2("val");
if (val?.length === 0) {
// catches "" and []
- // blur the input field so that pat-validate can kick in when
- // nothing was selected.
- this.el.dispatchEvent(events.blur_event());
+ // focus-out the input field so that pat-validate can kick in
+ // when nothing was selected.
+ this.el.dispatchEvent(events.focusout_event());
}
};
this.$el.on("select2-close", initiate_empty_check.bind(this));
diff --git a/src/pat/date-picker/date-picker.js b/src/pat/date-picker/date-picker.js
index 85142b5f6..8336620ad 100644
--- a/src/pat/date-picker/date-picker.js
+++ b/src/pat/date-picker/date-picker.js
@@ -173,9 +173,9 @@ export default Base.extend({
onSelect: () => this.dispatch_change_event(),
onClose: () => {
if (this.options.behavior === "styled" && !this.el.value) {
- // blur the input field so that pat-validate can kick in when
- // nothing was selected.
- el.dispatchEvent(events.blur_event());
+ // focus-out the input field so that pat-validate can kick
+ // in when nothing was selected.
+ el.dispatchEvent(events.focusout_event());
}
},
};
diff --git a/src/pat/validation/documentation.md b/src/pat/validation/documentation.md
index a3dc74fdd..fb03a7a63 100644
--- a/src/pat/validation/documentation.md
+++ b/src/pat/validation/documentation.md
@@ -20,6 +20,7 @@ These extra validation rules are:
- Equality checking between two fields (e.g. password confirmation).
- Date and datetime validation for before and after a given date or another input field.
+- 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.
### HTML form validation framework integration.
@@ -88,10 +89,10 @@ In addition both the input element and its label will get an `warning` class.
```
-Checkboxes and radio buttons are treated differently: if they are contained in a fieldset with class `checklist` error messages are added at the end of the fieldset.
+Checkboxes and radio buttons are treated differently: The error message is alywas set after the last element of the inputs with the same name.
```html
-
+
Demo with max-values / min-values support
+
+
+
+ `;
+ const form = document.querySelector(".pat-validation");
+ const input = document.querySelector("[name=outside]");
+ const button = document.querySelector("button");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ input.value = "";
+ input.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ // Skip, as jsDOM does not support the `:invalid` or `:valid`
+ // pseudo selectors on forms.
+ //expect(form.matches(":invalid")).toBe(true);
+ expect(input.matches(":invalid")).toBe(true);
+ expect(button.matches(":disabled")).toBe(true);
+ });
+
+ it("1.23 - Puts the warning at the end of same-name inputs.", async function () {
+ document.body.innerHTML = `
+
+ `;
+ const form = document.querySelector(".pat-validation");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ form.dispatchEvent(events.submit_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ const warning = form.querySelector("em.warning");
+ expect(warning).toBeTruthy();
+ expect(warning.matches("input:nth-child(3) + em.warning")).toBe(true);
+ });
+
+ it("1.24 - Supports dynamic forms.", async function () {
+ document.body.innerHTML = `
+
+ `;
+ const form = document.querySelector(".pat-validation");
+ const input1 = form.querySelector("[name=input1]");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ form.dispatchEvent(events.submit_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+
+ input1.value = "ok";
+ input1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(0);
+
+ const input2 = document.createElement("input");
+ input2.name = "input2";
+ input2.required = true;
+ form.appendChild(input2);
+
+ form.dispatchEvent(events.submit_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ expect(document.querySelector("em.warning").matches("input[name=input2] + em.warning")).toBe(true);
+ });
+
+ it("1.25 - Can combine multiple custom validation rules.", async function () {
+ document.body.innerHTML = `
+
+ `;
+ const form = document.querySelector(".pat-validation");
+ const input1 = document.querySelector("#i1");
+ const input2 = document.querySelector("#i2");
+ const input3 = document.querySelector("#i3");
+ const button = document.querySelector("button");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ input1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ // form is invalid due to input1's required attribute
+ expect(input1.matches(":invalid")).toBe(true);
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ expect(document.querySelectorAll("em.warning")[0].textContent).toBe(
+ "is required"
+ );
+
+ input1.value = "ok";
+ input1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ // form is invalid due to input1's equality constraint
+ expect(input1.matches(":invalid")).toBe(true);
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ expect(document.querySelectorAll("em.warning")[0].textContent).toBe(
+ "not equal"
+ );
+
+ input3.value = "ok";
+ input1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ // form is invalid due to input1's equality constraint
+ expect(input1.matches(":invalid")).toBe(true);
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ expect(document.querySelectorAll("em.warning")[0].textContent).toBe(
+ "too few"
+ );
+
+ input2.value = "ok";
+ form.dispatchEvent(events.submit_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ // form is now valid.
+ expect(input1.matches(":invalid")).toBe(false);
+ expect(document.querySelectorAll("em.warning").length).toBe(0);
+ });
});
describe("2 - required inputs", function () {
@@ -1509,4 +1658,213 @@ describe("pat-validation", function () {
expect(el.querySelector("#form-buttons-create").disabled).toBe(false);
});
});
+
+ describe("8 - min/max value validation", function () {
+ it("8.1 - validate min and max number of checked checkboxes", async function () {
+ document.body.innerHTML = `
+
+ `;
+
+ const form = document.querySelector("form");
+ const check1 = form.querySelector("[name=check][value='1']");
+ const check2 = form.querySelector("[name=check][value='2']");
+ const check3 = form.querySelector("[name=check][value='3']");
+ const check4 = form.querySelector("[name=check][value='4']");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ check1.checked = true;
+ check1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(check1.matches(":invalid")).toBe(true);
+ expect(check2.matches(":invalid")).toBe(true);
+ expect(check3.matches(":invalid")).toBe(true);
+ expect(check4.matches(":invalid")).toBe(true);
+ expect(form.querySelectorAll("em.warning").length).toBe(1);
+ expect(form.querySelectorAll("em.warning")[0].textContent).toBe(
+ "Please select at least 2 options"
+ );
+
+ check2.checked = true;
+ check2.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(check1.matches(":invalid")).toBe(false);
+ expect(check2.matches(":invalid")).toBe(false);
+ expect(check3.matches(":invalid")).toBe(false);
+ expect(check4.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+
+ check3.checked = true;
+ check3.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(check1.matches(":invalid")).toBe(false);
+ expect(check2.matches(":invalid")).toBe(false);
+ expect(check3.matches(":invalid")).toBe(false);
+ expect(check4.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+
+ check4.checked = true;
+ check4.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(check1.matches(":invalid")).toBe(true);
+ expect(check2.matches(":invalid")).toBe(true);
+ expect(check3.matches(":invalid")).toBe(true);
+ expect(check4.matches(":invalid")).toBe(true);
+ expect(form.querySelectorAll("em.warning").length).toBe(1);
+ expect(form.querySelectorAll("em.warning")[0].textContent).toBe(
+ "Please select at most 3 options"
+ );
+
+ check4.checked = false;
+ check4.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(check1.matches(":invalid")).toBe(false);
+ expect(check2.matches(":invalid")).toBe(false);
+ expect(check3.matches(":invalid")).toBe(false);
+ expect(check4.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+ });
+
+ it("8.2 - validate min and max number with mixed inputs", async function () {
+ document.body.innerHTML = `
+
+ `;
+
+ const form = document.querySelector("form");
+ const id1 = form.querySelector("#i1");
+ const id2 = form.querySelector("#i2");
+ const id3 = form.querySelector("#i3");
+ const id31 = form.querySelector("#i31");
+ const id4 = form.querySelector("#i4");
+ const id5 = form.querySelector("#i5");
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ id1.checked = true;
+ id1.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(true);
+ expect(id2.matches(":invalid")).toBe(true);
+ expect(id3.matches(":invalid")).toBe(true);
+ expect(id4.matches(":invalid")).toBe(true);
+ expect(id5.matches(":invalid")).toBe(true);
+ expect(form.querySelectorAll("em.warning").length).toBe(1);
+ expect(form.querySelectorAll("em.warning")[0].textContent).toBe(
+ "Please select at least 2 options"
+ );
+
+ id2.checked = true;
+ id2.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(false);
+ expect(id2.matches(":invalid")).toBe(false);
+ expect(id3.matches(":invalid")).toBe(false);
+ expect(id4.matches(":invalid")).toBe(false);
+ expect(id5.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+
+ id31.selected = true;
+ id3.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(false);
+ expect(id2.matches(":invalid")).toBe(false);
+ expect(id3.matches(":invalid")).toBe(false);
+ expect(id4.matches(":invalid")).toBe(false);
+ expect(id5.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+
+ id4.value = "okay";
+ id4.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(true);
+ expect(id2.matches(":invalid")).toBe(true);
+ expect(id3.matches(":invalid")).toBe(true);
+ expect(id4.matches(":invalid")).toBe(true);
+ expect(id5.matches(":invalid")).toBe(true);
+ expect(form.querySelectorAll("em.warning").length).toBe(1);
+ expect(form.querySelectorAll("em.warning")[0].textContent).toBe(
+ "Please select at most 3 options"
+ );
+
+ id2.checked = false;
+ id2.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(false);
+ expect(id2.matches(":invalid")).toBe(false);
+ expect(id3.matches(":invalid")).toBe(false);
+ expect(id4.matches(":invalid")).toBe(false);
+ expect(id5.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+
+ id5.value = "okay";
+ id4.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(true);
+ expect(id2.matches(":invalid")).toBe(true);
+ expect(id3.matches(":invalid")).toBe(true);
+ expect(id4.matches(":invalid")).toBe(true);
+ expect(id5.matches(":invalid")).toBe(true);
+ expect(form.querySelectorAll("em.warning").length).toBe(1);
+ expect(form.querySelectorAll("em.warning")[0].textContent).toBe(
+ "Please select at most 3 options"
+ );
+
+ id4.value = "";
+ id4.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(id1.matches(":invalid")).toBe(false);
+ expect(id2.matches(":invalid")).toBe(false);
+ expect(id3.matches(":invalid")).toBe(false);
+ expect(id4.matches(":invalid")).toBe(false);
+ expect(id5.matches(":invalid")).toBe(false);
+ expect(form.querySelectorAll("em.warning").length).toBe(0);
+ });
+
+ });
+
});