From 4e551a99920b9411fb61ebd5cedf127b6018fb36 Mon Sep 17 00:00:00 2001 From: Jordan Cannon Date: Fri, 11 Aug 2023 04:46:03 -0500 Subject: [PATCH] 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 ?? '';