Skip to content

Commit 735ebda

Browse files
committed
feat(pat-validation): Support dynamic forms.
Introduce event delegation and register input, change and focusout event handlers on the document level. This reduces the amount of registered event handlers which potentially improves performance and also supports dynamic forms where form elements can be added at any time.
1 parent 10dd63d commit 735ebda

File tree

5 files changed

+99
-42
lines changed

5 files changed

+99
-42
lines changed

src/pat/auto-suggest/auto-suggest.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ export default Base.extend({
122122
const val = $sel2.select2("val");
123123
if (val?.length === 0) {
124124
// catches "" and []
125-
// blur the input field so that pat-validate can kick in when
126-
// nothing was selected.
127-
this.el.dispatchEvent(events.blur_event());
125+
// focus-out the input field so that pat-validate can kick in
126+
// when nothing was selected.
127+
this.el.dispatchEvent(events.focusout_event());
128128
}
129129
};
130130
this.$el.on("select2-close", initiate_empty_check.bind(this));

src/pat/date-picker/date-picker.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ export default Base.extend({
173173
onSelect: () => this.dispatch_change_event(),
174174
onClose: () => {
175175
if (this.options.behavior === "styled" && !this.el.value) {
176-
// blur the input field so that pat-validate can kick in when
177-
// nothing was selected.
178-
el.dispatchEvent(events.blur_event());
176+
// focus-out the input field so that pat-validate can kick
177+
// in when nothing was selected.
178+
el.dispatchEvent(events.focusout_event());
179179
}
180180
},
181181
};

src/pat/validation/documentation.md

+6
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ pat-validation can handle structures like these:
126126
More information on the `form` attribute can be found at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form).
127127

128128

129+
### Dynamic forms
130+
131+
pat-validation supports dynamic forms where form elements are added after the Pattern was initialized.
132+
There is no need to re-initialize the pattern of to dispatch a special event.
133+
134+
129135
### Options reference
130136

131137
> **_NOTE:_** The form inputs must have a `name` attribute, otherwise the

src/pat/validation/validation.js

+50-34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Patterns validate - Form vlidation
22
import "../../core/polyfills"; // SubmitEvent.submitter for Safari < 15.4 and jsDOM
3-
import $ from "jquery";
43
import { BasePattern } from "../../core/basepattern";
54
import Parser from "../../core/parser";
65
import dom from "../../core/dom";
@@ -63,10 +62,56 @@ class Pattern extends BasePattern {
6362
{ capture: true }
6463
);
6564

66-
this.initialize_inputs();
67-
$(this.form).on("pat-update", () => {
68-
this.initialize_inputs();
69-
});
65+
// Input debouncer map:
66+
// - key: input element
67+
// - value: debouncer function
68+
// 1) We want do debounce the validation checks to avoid validating
69+
// while typing.
70+
// 2) We want to debounce the input events individually, so that we can
71+
// do multiple checks in parallel and show multiple errors at once.
72+
const input_debouncer_map = new Map();
73+
const debounce_filter = (e) => {
74+
const input = e.target;
75+
if (input?.form !== this.form || ! this.inputs.includes(input)) {
76+
// Ignore events from other forms or from elements which are
77+
// not inputs.
78+
return;
79+
}
80+
81+
if (! input_debouncer_map.has(input)) {
82+
// Create a new cancelable debouncer for this input.
83+
input_debouncer_map.set(input, utils.debounce((e) => {
84+
logger.debug("Checking input for event", input, e);
85+
this.check_input({ input: input, event: e });
86+
}, this.options.delay));
87+
}
88+
89+
// Get the debouncer for this input.
90+
const debouncer = input_debouncer_map.get(input);
91+
// Debounce the validation check.
92+
debouncer(input, e);
93+
};
94+
95+
events.add_event_listener(
96+
document,
97+
"input",
98+
`pat-validation--${this.uuid}--input--validator`,
99+
(e) => debounce_filter(e)
100+
);
101+
102+
events.add_event_listener(
103+
document,
104+
"change",
105+
`pat-validation--${this.uuid}--change--validator`,
106+
(e) => debounce_filter(e)
107+
);
108+
109+
events.add_event_listener(
110+
document,
111+
"focusout",
112+
`pat-validation--${this.uuid}--focusout--validator`,
113+
(e) => debounce_filter(e)
114+
);
70115

71116
// Set ``novalidate`` attribute to disable the browser's validation
72117
// bubbles but not disable the validation API.
@@ -94,35 +139,6 @@ class Pattern extends BasePattern {
94139
}
95140
}
96141

97-
initialize_inputs() {
98-
for (const [cnt, input] of this.inputs.entries()) {
99-
// Cancelable debouncer.
100-
const debouncer = utils.debounce((e) => {
101-
logger.debug("Checking input for event", input, e);
102-
this.check_input({ input: input, event: e });
103-
}, this.options.delay);
104-
105-
events.add_event_listener(
106-
input,
107-
"input",
108-
`pat-validation--input-${input.name}--${cnt}--validator`,
109-
(e) => debouncer(e)
110-
);
111-
events.add_event_listener(
112-
input,
113-
"change",
114-
`pat-validation--change-${input.name}--${cnt}--validator`,
115-
(e) => debouncer(e)
116-
);
117-
events.add_event_listener(
118-
input,
119-
"blur",
120-
`pat-validation--blur-${input.name}--${cnt}--validator`,
121-
(e) => debouncer(e)
122-
);
123-
}
124-
}
125-
126142
check_input({
127143
input, // Input to check.
128144
event = null, // Optional event which triggered the check.

src/pat/validation/validation.test.js

+37-2
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ describe("pat-validation", function () {
477477
const instance = new Pattern(el);
478478
await events.await_pattern_init(instance);
479479

480-
document.querySelector("[name=i1]").dispatchEvent(events.blur_event());
480+
document.querySelector("[name=i1]").dispatchEvent(events.focusout_event());
481481
await utils.timeout(1); // wait a tick for async to settle.
482482

483483
expect(el.querySelectorAll("em.warning").length).toBe(2);
@@ -498,7 +498,7 @@ describe("pat-validation", function () {
498498
const instance = new Pattern(el);
499499
await events.await_pattern_init(instance);
500500

501-
document.querySelector("[name=i1]").dispatchEvent(events.blur_event());
501+
document.querySelector("[name=i1]").dispatchEvent(events.focusout_event());
502502
await utils.timeout(1); // wait a tick for async to settle.
503503

504504
expect(el.querySelectorAll("em.warning").length).toBe(1);
@@ -585,6 +585,41 @@ describe("pat-validation", function () {
585585
expect(warning).toBeTruthy();
586586
expect(warning.matches("input:nth-child(3) + em.warning")).toBe(true);
587587
});
588+
589+
it("1.24 - Supports dynamic forms.", async function () {
590+
document.body.innerHTML = `
591+
<form class="pat-validation" id="form">
592+
<input name="input1" required/>
593+
</form>
594+
`;
595+
const form = document.querySelector(".pat-validation");
596+
const input1 = form.querySelector("[name=input1]");
597+
598+
const instance = new Pattern(form);
599+
await events.await_pattern_init(instance);
600+
601+
form.dispatchEvent(events.submit_event());
602+
await utils.timeout(1); // wait a tick for async to settle.
603+
604+
expect(document.querySelectorAll("em.warning").length).toBe(1);
605+
606+
input1.value = "ok";
607+
input1.dispatchEvent(events.change_event());
608+
await utils.timeout(1); // wait a tick for async to settle.
609+
610+
expect(document.querySelectorAll("em.warning").length).toBe(0);
611+
612+
const input2 = document.createElement("input");
613+
input2.name = "input2";
614+
input2.required = true;
615+
form.appendChild(input2);
616+
617+
form.dispatchEvent(events.submit_event());
618+
await utils.timeout(1); // wait a tick for async to settle.
619+
620+
expect(document.querySelectorAll("em.warning").length).toBe(1);
621+
expect(document.querySelector("em.warning").matches("input[name=input2] + em.warning")).toBe(true);
622+
});
588623
});
589624

590625
describe("2 - required inputs", function () {

0 commit comments

Comments
 (0)