diff --git a/src/core/basepattern.js b/src/core/basepattern.js index cf7975626..733e556d9 100644 --- a/src/core/basepattern.js +++ b/src/core/basepattern.js @@ -104,6 +104,16 @@ class BasePattern { // Extend this method in your pattern. } + emit_update(action = undefined, options = {}) { + options = { + pattern: this.name, + dom: this.el, + action: action, + ...options, + } + this.el.dispatchEvent(events.update_event(options)); + } + /** * Listen to an event on the element only once. * diff --git a/src/core/basepattern.test.js b/src/core/basepattern.test.js index 4b3a43f1f..8547f95b3 100644 --- a/src/core/basepattern.test.js +++ b/src/core/basepattern.test.js @@ -364,4 +364,29 @@ describe("Basepattern class tests", function () { // If test reaches this expect statement, the init event catched. expect(true).toBe(true); }); + + it("7 - Emit update event.", async function () { + const events = (await import("./events")).default; + class Pat extends BasePattern { + static name = "example"; + static trigger = ".example"; + } + + const el = document.createElement("div"); + el.classList.add("example"); + + const pat = new Pat(el); + await events.await_pattern_init(pat); + + let event; + el.addEventListener("pat-update", (e) => { + event = e; + }); + + pat.emit_update("test"); + + expect(event.detail.pattern).toBe("example"); + expect(event.detail.dom).toBe(el); + expect(event.detail.action).toBe("test"); + }); }); diff --git a/src/core/events.js b/src/core/events.js index 9919d8c67..724c43546 100644 --- a/src/core/events.js +++ b/src/core/events.js @@ -181,6 +181,28 @@ const generic_event = (name) => { }); }; + +/** Patternslib specifc event factories + */ + +class UpdateEvent extends CustomEvent { + constructor(options) { + super("pat-update", { + bubbles: true, + cancelable: true, + detail: options + }); + } +} + +const update_event = (options) => { + return new UpdateEvent(options); +} + + +/** Web API event factories + */ + const blur_event = () => { return new Event("blur", { bubbles: false, @@ -266,6 +288,7 @@ export default { await_event: await_event, await_pattern_init: await_pattern_init, generic_event: generic_event, + update_event: update_event, blur_event: blur_event, click_event: click_event, change_event: change_event, diff --git a/src/core/events.test.js b/src/core/events.test.js index 030fa94fc..42f03b98a 100644 --- a/src/core/events.test.js +++ b/src/core/events.test.js @@ -467,6 +467,26 @@ describe("core.events tests", () => { expect(catched).toBe("outer"); }); + it("update event", async () => { + outer.addEventListener("pat-update", () => { + catched = "outer"; + }); + inner.dispatchEvent(events.update_event()); + await utils.timeout(1); + expect(catched).toBe("outer"); + }); + + it("update event with data", async () => { + let event; + outer.addEventListener("pat-update", (e) => { + event = e; + }); + const data = {"foo": "bar"}; + inner.dispatchEvent(events.update_event(data)); + await utils.timeout(1); + expect(event.detail).toBe(data); + }); + it("blur event", async () => { outer.addEventListener("blur", () => { catched = "outer"; diff --git a/src/pat/validation/validation.js b/src/pat/validation/validation.js index 04acdf5a3..53421d41d 100644 --- a/src/pat/validation/validation.js +++ b/src/pat/validation/validation.js @@ -113,7 +113,7 @@ class Pattern extends BasePattern { } // In any case, clear the custom validity first. - this.set_validity({ input: input, msg: "" }); + this.set_error({ input: input, msg: "", skip_event: true }); const validity_state = input.validity; if (event?.submitter?.hasAttribute("formnovalidate")) { @@ -148,7 +148,7 @@ class Pattern extends BasePattern { const message = input_options.message.equality || `The value is not equal to %{attribute}`; - this.set_validity({ + this.set_error({ input: input, msg: message, attribute: input_options.equality, @@ -216,7 +216,7 @@ class Pattern extends BasePattern { ); msg_attr = msg_attr || not_after_el.name; } - this.set_validity({ + this.set_error({ input: input, msg: msg || msg_default_not_after, attribute: msg_attr.trim(), @@ -237,7 +237,7 @@ class Pattern extends BasePattern { ); msg_attr = msg_attr || not_before_el.name; } - this.set_validity({ + this.set_error({ input: input, msg: msg || msg_default_not_before, attribute: msg_attr.trim(), @@ -266,15 +266,15 @@ class Pattern extends BasePattern { // Default error cases with custom messages. if (validity_state.valueMissing && input_options.message.required) { - this.set_validity({ input: input, msg: input_options.message.required }); + this.set_error({ input: input, msg: input_options.message.required }); } else if (validity_state.rangeUnderflow && input_options.message.min) { - this.set_validity({ + this.set_error({ input: input, msg: input_options.message.min, min: input.getAttribute("min"), }); } else if (validity_state.rangeOverflow && input_options.message.max) { - this.set_validity({ + this.set_error({ input: input, msg: input_options.message.max, max: input.getAttribute("max"), @@ -284,38 +284,43 @@ class Pattern extends BasePattern { input.type === "number" && input_options.message.number ) { - this.set_validity({ input: input, msg: input_options.message.number }); + this.set_error({ input: input, msg: input_options.message.number }); } else if ( validity_state.typeMismatch && input.type === "email" && input_options.message.email ) { - this.set_validity({ input: input, msg: input_options.message.email }); + this.set_error({ input: input, msg: input_options.message.email }); } else if ( validity_state.rangeUnderflow && input.type === "date" && input_options.message.date ) { - this.set_validity({ input: input, msg: input_options.message.date }); + this.set_error({ input: input, msg: input_options.message.date }); } else if ( validity_state.rangeOverflow && input.type === "date" && input_options.message.date ) { - this.set_validity({ input: input, msg: input_options.message.date }); + this.set_error({ input: input, msg: input_options.message.date }); } else if ( validity_state.rangeUnderflow && input.type === "datetime" && input_options.message.datetime ) { - this.set_validity({ input: input, msg: input_options.message.datetime }); + this.set_error({ input: input, msg: input_options.message.datetime }); } else if ( validity_state.rangeOverflow && input.type === "datetime" && input_options.message.datetime ) { - this.set_validity({ input: input, msg: input_options.message.datetime }); + this.set_error({ input: input, msg: input_options.message.datetime }); + } else { + // Still an error, but without customized messages. + // Call `emit_update` separately + this.emit_update("invalid"); } + } if (event?.type === "submit") { @@ -327,7 +332,7 @@ class Pattern extends BasePattern { this.set_error_message(input); } - set_validity({ input, msg, attribute = null, min = null, max = null }) { + set_error({ input, msg, attribute = null, min = null, max = null, skip_event = false }) { // Replace some variables, as like validate.js if (attribute) { msg = msg.replace(/%{attribute}/g, attribute); @@ -345,9 +350,13 @@ class Pattern extends BasePattern { // Hidden inputs do not participate in validation but we need this // (e.g. styled date input). input[KEY_ERROR_MSG] = msg; + + if (!skip_event) { + this.emit_update("invalid"); + } } - remove_error(input, all_of_group = false) { + remove_error(input, all_of_group = false, skip_event = false) { // Remove error message and related referencesfrom input. let inputs = [input]; @@ -370,10 +379,15 @@ class Pattern extends BasePattern { } } } + + if (!skip_event) { + this.emit_update("valid"); + } } set_error_message(input) { - this.remove_error(input); + // First, remove the old error message. + this.remove_error(input, false, true); // Do not set a error message for a input group like radio buttons or // checkboxes where one has already been set. diff --git a/src/pat/validation/validation.test.js b/src/pat/validation/validation.test.js index ef4a0cf50..b50f5e15c 100644 --- a/src/pat/validation/validation.test.js +++ b/src/pat/validation/validation.test.js @@ -503,6 +503,41 @@ describe("pat-validation", function () { expect(el.querySelectorAll("em.warning").length).toBe(1); }); + it("1.21 - Emits an update event when the validation state changes", async function () { + document.body.innerHTML = ` +
+ +
+ `; + const el = document.querySelector(".pat-validation"); + const inp = el.querySelector("[name=name]"); + + const instance = new Pattern(el); + await events.await_pattern_init(instance); + + let event; + el.addEventListener("pat-update", (e) => { + event = e; + }); + + inp.value = ""; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + + expect(el.querySelectorAll("em.warning").length).toBe(1); + expect(event.detail.pattern).toBe("validation"); + expect(event.detail.dom).toBe(el); + expect(event.detail.action).toBe("invalid"); + + inp.value = "okay"; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + + expect(event.detail.pattern).toBe("validation"); + expect(event.detail.dom).toBe(el); + expect(event.detail.action).toBe("valid"); + }); + it("2.1 - validates required inputs", async function () { document.body.innerHTML = `