Skip to content

Commit 0e25a09

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 0e25a09

File tree

5 files changed

+99
-41
lines changed

5 files changed

+99
-41
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-33
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,56 @@ class Pattern extends BasePattern {
6363
{ capture: true }
6464
);
6565

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

71117
// Set ``novalidate`` attribute to disable the browser's validation
72118
// bubbles but not disable the validation API.
@@ -94,35 +140,6 @@ class Pattern extends BasePattern {
94140
}
95141
}
96142

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-
126143
check_input({
127144
input, // Input to check.
128145
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)