From 4e551a99920b9411fb61ebd5cedf127b6018fb36 Mon Sep 17 00:00:00 2001 From: Jordan Cannon Date: Fri, 11 Aug 2023 04:46:03 -0500 Subject: [PATCH 1/3] Listen for removed inputs and unregister them --- src/index.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2713e30..a38cd62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -518,13 +518,17 @@ export class ValidationService { /** * Scans document for all validation message generated by ASP.NET Core MVC, then tracks them. */ - private scanMessages(root: ParentNode) { + private scanMessages(root: ParentNode, remove: boolean = false) { /* If a validation span explicitly declares a form, we group the span with that form. */ let validationMessageElements = Array.from(root.querySelectorAll('span[form]')); for (let span of validationMessageElements) { let form = document.getElementById(span.getAttribute('form')); if (form) { - this.pushValidationMessageSpan(form, span); + if (remove) { + this.removeValidationMessageSpan(form, span); + } else { + this.pushValidationMessageSpan(form, span); + } } } @@ -540,7 +544,11 @@ export class ValidationService { let validationMessageElements = Array.from(form.querySelectorAll('[data-valmsg-for]')); for (let span of validationMessageElements) { - this.pushValidationMessageSpan(form, span); + if (remove) { + this.removeValidationMessageSpan(form, span); + } else { + this.pushValidationMessageSpan(form, span); + } } } } @@ -557,6 +565,19 @@ export class ValidationService { } } + private removeValidationMessageSpan(form: HTMLElement, span: HTMLElement) { + let formId = this.getElementUID(form); + let name = `${formId}:${span.getAttribute('data-valmsg-for')}`; + let spans = this.messageFor[name] || (this.messageFor[name] = []); + let index = spans.indexOf(span); + if (index >= 0) { + spans.splice(index, 1); + } + else { + this.logger.log("Validation element for '%s' was already removed", name, span); + } + } + /** * Given attribute map for an HTML input, returns the validation directives to be executed. * @param attributes @@ -649,7 +670,10 @@ export class ValidationService { for (let i = 0; i < formInputUIDs.length; i++) { let inputUID = formInputUIDs[i]; - formValidators.push(this.validators[inputUID]); + const validator = this.validators[inputUID]; + if (validator) { + formValidators.push(validator); + } } let tasks = formValidators.map(factory => factory()); @@ -895,6 +919,20 @@ export class ValidationService { this.elementEvents[formUID] = cb; } + private untrackFormInput(form: HTMLFormElement, inputUID: string) { + let formUID = this.getElementUID(form); + if (!this.formInputs[formUID]) { + this.formInputs[formUID] = []; + } + let indexToRemove = this.formInputs[formUID].indexOf(inputUID); + if (indexToRemove >= 0) { + this.formInputs[formUID].splice(indexToRemove, 1); + } + else { + this.logger.log("Form input for UID '%s' was already removed", inputUID); + } + } + /** * Adds an input element to be managed and validated by the service. * Triggers a debounced live validation when input value changes. @@ -935,10 +973,22 @@ export class ValidationService { this.elementEvents[uid] = cb; } + removeInput(input: ValidatableElement) { + let uid = this.getElementUID(input); + + delete this.summary[uid]; + delete this.elementEvents[uid]; + delete this.validators[uid]; + + if (input.form) { + this.untrackFormInput(input.form, uid); + } + } + /** * Scans the entire document for input elements to be validated. */ - private scanInputs(root: ParentNode) { + private scanInputs(root: ParentNode, remove: boolean = false) { let inputs = Array.from(root.querySelectorAll(validatableSelector('[data-val="true"]'))); // querySelectorAll does not include the root element itself. @@ -949,7 +999,12 @@ export class ValidationService { for (let i = 0; i < inputs.length; i++) { let input = inputs[i]; - this.addInput(input); + if (remove) { + this.removeInput(input); + } + else { + this.addInput(input); + } } } @@ -1210,10 +1265,10 @@ export class ValidationService { /** * Scans the provided root element for any validation directives and attaches behavior to them. */ - scan(root: ParentNode) { + scan(root: ParentNode, remove: boolean = false) { this.logger.log('Scanning', root); - this.scanMessages(root); - this.scanInputs(root); + this.scanMessages(root, remove); + this.scanInputs(root, remove); } /** @@ -1243,6 +1298,13 @@ export class ValidationService { this.scan(node); } } + for (let i = 0; i < mutation.removedNodes.length; i++) { + let node = mutation.removedNodes[i]; + this.logger.log('Removed node', node); + if (node instanceof HTMLElement) { + this.scan(node, true); + } + } } else if (mutation.type === 'attributes') { if (mutation.target instanceof HTMLElement) { const oldValue = mutation.oldValue ?? ''; From ea66b6af6c65e5728fe3a73e019e4ce793b4bf98 Mon Sep 17 00:00:00 2001 From: Jordan Cannon Date: Mon, 14 Aug 2023 01:02:55 -0500 Subject: [PATCH 2/3] Return early when values don't exist --- src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index a38cd62..f3b9045 100644 --- a/src/index.ts +++ b/src/index.ts @@ -568,7 +568,10 @@ export class ValidationService { private removeValidationMessageSpan(form: HTMLElement, span: HTMLElement) { let formId = this.getElementUID(form); let name = `${formId}:${span.getAttribute('data-valmsg-for')}`; - let spans = this.messageFor[name] || (this.messageFor[name] = []); + let spans = this.messageFor[name]; + if (!spans) { + return; + } let index = spans.indexOf(span); if (index >= 0) { spans.splice(index, 1); @@ -922,7 +925,7 @@ export class ValidationService { private untrackFormInput(form: HTMLFormElement, inputUID: string) { let formUID = this.getElementUID(form); if (!this.formInputs[formUID]) { - this.formInputs[formUID] = []; + return; } let indexToRemove = this.formInputs[formUID].indexOf(inputUID); if (indexToRemove >= 0) { From f1be447ffa6a90238bb32279b0b352da491b85d0 Mon Sep 17 00:00:00 2001 From: Jordan Cannon Date: Mon, 14 Aug 2023 01:08:03 -0500 Subject: [PATCH 3/3] Add separate public remove function instead of passing a boolean to scan --- src/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index f3b9045..876ec75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1268,10 +1268,19 @@ export class ValidationService { /** * Scans the provided root element for any validation directives and attaches behavior to them. */ - scan(root: ParentNode, remove: boolean = false) { + scan(root: ParentNode) { this.logger.log('Scanning', root); - this.scanMessages(root, remove); - this.scanInputs(root, remove); + this.scanMessages(root); + this.scanInputs(root); + } + + /** + * Scans the provided root element for any validation directives and removes behavior from them. + */ + remove(root: ParentNode) { + this.logger.log('Removing', root); + this.scanMessages(root, true); + this.scanInputs(root, true); } /** @@ -1305,7 +1314,7 @@ export class ValidationService { let node = mutation.removedNodes[i]; this.logger.log('Removed node', node); if (node instanceof HTMLElement) { - this.scan(node, true); + this.remove(node); } } } else if (mutation.type === 'attributes') {