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..8494253c 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,23 @@ export class Dispatcher implements BindingObserverDelegate { this.application.handleError(error, `Error ${message}`, detail) } + private clearEventListenersForBinding(binding: Binding) { + const eventListener = this.fetchEventListenerForBinding(binding) + if (!eventListener.hasBindings()) { + eventListener.disconnect() + this.removeMappedEventListenerFor(binding) + } + } + + private removeMappedEventListenerFor(binding: Binding) { + const { eventTarget, eventName, eventOptions } = binding + const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget) + const cacheKey = this.cacheKey(eventName, eventOptions) + + 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/core/event_listener.ts b/src/core/event_listener.ts index f78680d6..1b849e47 100644 --- a/src/core/event_listener.ts +++ b/src/core/event_listener.ts @@ -43,6 +43,10 @@ export class EventListener implements EventListenerObject { } } + hasBindings() { + return this.unorderedBindings.size > 0 + } + get bindings(): Binding[] { return Array.from(this.unorderedBindings).sort((left, right) => { const leftIndex = left.index, 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) + } +}