Skip to content

Commit ee91c07

Browse files
committed
added new event listeners cleaner
1 parent e008174 commit ee91c07

File tree

2 files changed

+66
-17
lines changed

2 files changed

+66
-17
lines changed

src/elements.ts

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ interface EventListenerOptions {
4343
export abstract class BaseView<T extends HTMLElement|SVGElement> {
4444
readonly _data: Obj<unknown> = {};
4545
readonly _events: Obj<EventCallback[]> = {};
46+
_mutationObserver: MutationObserver|undefined;
47+
private readonly _mutationObserverCallbacks: Obj<EventCallback[]> = {};
4648
readonly type: string = 'default';
4749
model?: Observable;
50+
isDeleted?: boolean;
4851

4952
constructor(readonly _el: T) {
5053
// Store a reference to this element within the native browser DOM.
@@ -628,9 +631,27 @@ export abstract class BaseView<T extends HTMLElement|SVGElement> {
628631
}
629632
}
630633

634+
/** Removes listeners and model data from el */
635+
private unsubscribe() {
636+
this.isDeleted = true;
637+
if (!Browser.isSafari) this.model?.clear();
638+
this._mutationObserver?.disconnect();
639+
this.offAll();
640+
641+
for (const child of this.children) {
642+
child.unsubscribe();
643+
}
644+
645+
delete (this as any)._data;
646+
delete (this as any)._events;
647+
delete (this as any)._mutationObserverCallbacks;
648+
}
649+
631650
/** Removes this element. */
632651
remove() {
633652
this.detach();
653+
this.unsubscribe();
654+
634655
// TODO Remove event listeners (including children)
635656
// TODO Remove model bindings (including children)
636657
// this._el = this._data = this._events = undefined;
@@ -679,13 +700,27 @@ export abstract class BaseView<T extends HTMLElement|SVGElement> {
679700
*/
680701
off(events: string, callback?: EventCallback) {
681702
for (const e of words(events)) {
682-
if (e in this._events) {
683-
this._events[e] = callback ? this._events[e].filter(fn => fn !== callback) : [];
703+
if (callback) {
704+
this._events[e] = this._events[e].filter(fn => fn !== callback);
705+
unbindEvent(this, e, callback);
706+
continue;
684707
}
685-
unbindEvent(this, e, callback);
708+
if (this._events[e]) {
709+
for (const eventsCallback of this._events[e]) unbindEvent(this, e, eventsCallback);
710+
}
711+
this._events[e] = [];
686712
}
687713
}
688714

715+
/**
716+
* Removes all event listeners from this element
717+
*/
718+
offAll() {
719+
Object.entries(this._events).forEach(([eventName, callbacks]) => {
720+
callbacks.forEach((callback) => this.off(eventName, callback));
721+
});
722+
}
723+
689724
/** Triggers a specific event on this element. */
690725
trigger(events: string, args: unknown = {}) {
691726
for (const e of words(events)) {
@@ -703,28 +738,40 @@ export abstract class BaseView<T extends HTMLElement|SVGElement> {
703738
const keyNames = new Set(words(keys));
704739
const event = options?.up ? 'keyup' : 'keydown';
705740

706-
const target = (this._el === document.body ? document : this._el) as HTMLElement;
707-
target.addEventListener(event, (e: KeyboardEvent) => {
741+
const eventFunction = (e: KeyboardEvent) => {
708742
const key = keyCode(e);
709743
if (options?.meta ? !e.ctrlKey && !e.metaKey : e.ctrlKey || e.metaKey) return;
710744
if (!key || !keyNames.has(key)) return;
711745
if (document.activeElement !== this._el && document.activeElement?.shadowRoot?.activeElement !== this._el && Browser.formIsActive) return;
712746
callback(e as KeyboardEvent, key);
713-
});
747+
};
748+
749+
const target = (this._el === document.body ? document : this._el) as HTMLElement;
750+
target.addEventListener(event, eventFunction);
751+
752+
if (!(event in this._events)) this._events[event] = [];
753+
this._events[event].push(eventFunction);
714754
}
715755

756+
/**
757+
* Bind an listener when element attribute changed
758+
*/
716759
onAttr(name: string, callback: (value: string, initial?: boolean) => void) {
717-
// TODO Reuse existing observers, remove events, disconnect when deleting.
718-
719-
const observer = new MutationObserver((mutations) => {
720-
for (const m of mutations) {
721-
if (m.type === 'attributes' && m.attributeName === name) {
722-
callback(this.attr(name));
760+
if (!this._mutationObserver) {
761+
this._mutationObserver = new MutationObserver((mutations) => {
762+
for (const m of mutations) {
763+
if (m.type === 'attributes' && m.attributeName && m.attributeName in this._mutationObserverCallbacks) {
764+
for (const attributeCallback of this._mutationObserverCallbacks[m.attributeName]) {
765+
attributeCallback(this.attr(m.attributeName));
766+
}
767+
}
723768
}
724-
}
725-
});
769+
});
770+
this._mutationObserver.observe(this._el, {attributes: true});
771+
}
726772

727-
observer.observe(this._el, {attributes: true});
773+
if (!(name in this._mutationObserverCallbacks)) this._mutationObserverCallbacks[name] = [];
774+
this._mutationObserverCallbacks[name].push(callback);
728775
callback(this.attr(name), true);
729776
}
730777

src/observable.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// (c) Mathigon
44
// =============================================================================
55

6+
import {Browser} from './browser';
7+
68

79
type Callback<T> = (state: T, initial?: boolean) => void;
810
type Expr<T> = (state: T) => void;
@@ -111,7 +113,7 @@ export function observe<T extends object = any>(state: T, parentModel?: Observab
111113
}
112114

113115
function assign(changes: Partial<T>, clear?: boolean) {
114-
if (clear) state = {} as T;
116+
if (clear && !Browser.isSafari) state = {} as T;
115117
batch(() => {
116118
for (const [key, value] of Object.entries(changes)) {
117119
if (!(key in previous)) (previous as any)[key] = (state as any)[key];
@@ -127,10 +129,10 @@ export function observe<T extends object = any>(state: T, parentModel?: Observab
127129
}
128130

129131
function clear() {
130-
state = {} as T;
131132
callbackMap.clear();
132133
computedKeys.clear();
133134
watchAllCallbacks.clear();
135+
if (!Browser.isSafari) state = {} as T;
134136
lastKey = 0;
135137
}
136138

0 commit comments

Comments
 (0)