diff --git a/src/core/binding_observer.ts b/src/core/binding_observer.ts index 99a5019f..fd56295e 100644 --- a/src/core/binding_observer.ts +++ b/src/core/binding_observer.ts @@ -7,7 +7,7 @@ import { Token, ValueListObserver, ValueListObserverDelegate } from "../mutation export interface BindingObserverDelegate extends ErrorHandler { bindingConnected(binding: Binding): void - bindingDisconnected(binding: Binding): void + bindingDisconnected(binding: Binding, clearEventListeners?: boolean): void } export class BindingObserver implements ValueListObserverDelegate { @@ -72,7 +72,7 @@ export class BindingObserver implements ValueListObserverDelegate { } private disconnectAllActions() { - this.bindings.forEach((binding) => this.delegate.bindingDisconnected(binding)) + this.bindings.forEach((binding) => this.delegate.bindingDisconnected(binding, true)) this.bindingsByAction.clear() } diff --git a/src/core/dispatcher.ts b/src/core/dispatcher.ts index cfa9e365..5cabae33 100644 --- a/src/core/dispatcher.ts +++ b/src/core/dispatcher.ts @@ -41,8 +41,9 @@ export class Dispatcher implements BindingObserverDelegate { this.fetchEventListenerForBinding(binding).bindingConnected(binding) } - bindingDisconnected(binding: Binding) { + bindingDisconnected(binding: Binding, clearEventListeners: boolean = false) { this.fetchEventListenerForBinding(binding).bindingDisconnected(binding) + if (clearEventListeners) this.clearEventListenersForBinding(binding) } // Error handling @@ -51,6 +52,19 @@ export class Dispatcher implements BindingObserverDelegate { this.application.handleError(error, `Error ${message}`, detail) } + private clearEventListenersForBinding(binding: Binding) { + const eventListener = this.fetchEventListenerForBinding(binding) + if (eventListener.bindings.length == 0) { + const { eventTarget, eventName, eventOptions } = binding + const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget) + const cacheKey = this.cacheKey(eventName, eventOptions) + eventListener.disconnect() + eventListenerMap.delete(cacheKey) + + if (eventListenerMap.size == 0) this.eventListenerMaps.delete(eventTarget) + } + } + private fetchEventListenerForBinding(binding: Binding): EventListener { const { eventTarget, eventName, eventOptions } = binding return this.fetchEventListener(eventTarget, eventName, eventOptions) diff --git a/src/tests/modules/core/memory_tests.ts b/src/tests/modules/core/memory_tests.ts new file mode 100644 index 00000000..ae73a28c --- /dev/null +++ b/src/tests/modules/core/memory_tests.ts @@ -0,0 +1,22 @@ +import { ControllerTestCase } from "../../cases/controller_test_case" + +export default class MemoryTests extends ControllerTestCase() { + controllerElement!: Element + + async setup() { + this.controllerElement = this.controller.element + } + + fixtureHTML = ` +
+ + +
+ ` + + async "test removing a controller clears dangling eventListeners"() { + this.assert.equal(this.application.dispatcher.eventListeners.length, 2) + await this.fixtureElement.removeChild(this.controllerElement) + this.assert.equal(this.application.dispatcher.eventListeners.length, 0) + } +}