@@ -43,8 +43,11 @@ interface EventListenerOptions {
4343export 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
0 commit comments