Announcements

Help Wizard

Step 1

NEXT STEP

removeListener not working inside addEventListener handler

Solved!

removeListener not working inside addEventListener handler

Plan

Free

Country

US

Device

 Macbook Pro

Operating System

MacOS Sonoma

 

My Question or Issue

I'm using a stimulus controller to initialize the iframe embed. I'm adding an event listener to the EmbedController that works fine and I want to remove it when the form is selected. I'm not sure what I'm doing wrong but the removeListener function is not working for the EmbedController. I've gone into debugger, the textArea event is being triggered and I have access to EmbedController.removeListener but it returns undefined when I call 

EmbedController.removeListener('playback_update', playBackPositionBroadcast)

in the debugger. I can see the event listed under _listeners on EmbedController. I'm not sure where to go from here. My code is below. Any help is appreciated🙂


const callback = (EmbedController) => {
const playBackPositionBroadcast = (playbackEvent) => {
///code to send playbackEvent.data.position to form in the dom
};
EmbedController.addListener('playback_update', playBackPositionBroadcast);
this.textAreaTarget.addEventListener('click', () => {
EmbedController.removeListener('playback_update', playBackPositionBroadcast);
});
};

 

Reply

Accepted Solutions
Marked as solution

It feels a bit janky but I got it working by redefining EmbedController._listeners['playback_update'] = []

So my working code is:

const callback = (EmbedController) => {
          const playBackPositionBroadcast = (playbackEvent) => {
   /// broadcast to dom
          };

          const removePlaybackListener = () => {
            EmbedController._listeners['playback_update'] = [];
          };
          const addPlaybackListener = () => {
            EmbedController.addListener('playback_update', playBackPositionBroadcast);
          };
    
          
          this.textAreaTarget.addEventListener('focus', removePlaybackListener);
          this.textAreaTarget.addEventListener('blur', addPlaybackListener);
          EmbedController.addListener('playback_update', playBackPositionBroadcast);
        };

View solution in original post

7 Replies

Howldy ZachAttax!

It seems there might be an issue with scoping or context when trying to remove the listener. In your code, you're using an arrow function for the click event listener, which might affect the scope of this. Also, the this keyword might refer to the global context rather than the expected context within the callback.

 

Try capturing the reference to playBackPositionBroadcast outside the callback function, and make sure to use a named function for the click event listener to ensure proper scoping:

const callback = (EmbedController) => {
  const playBackPositionBroadcast = (playbackEvent) => {
    // code to send playbackEvent.data.position to form in the dom
  };

  const removePlaybackListener = () => {
    EmbedController.removeListener('playback_update', playBackPositionBroadcast);
    this.textAreaTarget.removeEventListener('click', removePlaybackListener);
  };

  EmbedController.addListener('playback_update', playBackPositionBroadcast);
  this.textAreaTarget.addEventListener('click', removePlaybackListener);
};


By defining a named function (removePlaybackListener), it ensures that the same reference is used for adding and removing the event listener. Also, using removeEventListener instead of removeListener for the textAreaTarget should properly remove the click event listener when triggered.

Bark some more orders at me if this doesn't solve the issue and we'll get it sorted,

 

-Prague the Dog

PragueSpotify Star
Help others find this answer and click "Accept as Solution".
If you appreciate my answer, maybe give me a Like.
Note: I'm not a Spotify employee.

I think you're right that it is a context issue. I tried your suggestion of using named function for the event callback. I also changed the event from to 'click' to 'blur' and 'focus'. Using the debugger I'm getting the expected context of this as my stimulus controller. I can also see that the 'click' and 'blur' events are being triggered correctly but the EmbedController events are still not being removed. Here is my updated code.

        const callback = (EmbedController) => {
          const playBackPositionBroadcast = (playbackEvent) => {
           // broadcast code
          };

          const removePlaybackListener = () => {
            EmbedController.removeListener('playback_update', playBackPositionBroadcast);
          };
          const addPlaybackListener = () => {
            EmbedController.addListener('playback_update', playBackPositionBroadcast);
          };
    
          EmbedController.addListener('playback_update', playBackPositionBroadcast);

          this.textAreaTarget.addEventListener('focus', removePlaybackListener);
          this.textAreaTarget.addEventListener('blur', addPlaybackListener);
        };

Sniffing around this a bit more it seems like despite correctly setting up the event listeners for 'blur' and 'focus' on this.textAreaTarget, the issue might persist due to the EmbedController's removeListener and addListener methods not functioning as expected.

 

Ensure the event names are accurate and the EmbedController methods are being used correctly. Debugging with console logs or a debugger might help trace the execution flow within the EmbedController.

 

Consider checking if the removeListener and addListener methods are implemented as intended in the EmbedController. Sometimes, specific nuances or requirements might exist regarding event handling within that library or framework.

Getting closer?
Top paw regards,

 

-Prague the Dog

 

 

 

PragueSpotify Star
Help others find this answer and click "Accept as Solution".
If you appreciate my answer, maybe give me a Like.
Note: I'm not a Spotify employee.

This is from the docs


playback_update

This event fires after the state of playback changes. Playback state changes can occur by the user tapping on the playback controls in the Embed, or programmatically through methods of the iFrame API, such as the seek() method.

The event handler will receive an object with four properties describing the current playback state: isPaused (bool), isBuffering (bool), duration (number) and position (number). duration describes the length of the loaded podcast episode in miliseconds and position describes the cursor location in miliseconds.


Code sample

 

 

EmbedController.addListener('playback_update', e => {
  document.getElementById('progressTimestamp').innerText = `${parseInt(e.data.position / 1000, 10)} s`;
  });

 

 

 

I'm not seeing any documentation for removeListener but it is in the source code on line 48

 

 

        this.removeListener = (eventName, handler) => {
            if (!this._listeners[eventName] || !this._listeners[eventName].length) {
                this._listeners[eventName] = this._listeners[eventName].filter(storedHandler => handler !== storedHandler);
            }
        };

 

 


I'm not sure how I could debug the EmbedController since it's part of the Spotify API



full source code

 

 

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @spotify-internal/uri */ "./node_modules/@spotify-internal/uri/lib/index.js");
/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ "./src/iframe-api/src/v1/constants.ts");
/* harmony import */ var _types__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./types */ "./src/iframe-api/src/v1/types.ts");



let SpotifyIframeApi;
class SpotifyEmbedController {
    constructor(targetElement, options) {
        var _a, _b, _c, _d, _e;
        this._listeners = {
            [_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.READY]: [],
            [_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.PLAYBACK_UPDATE]: [],
            [_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR]: [],
        };
        this.currentUri = '';
        this.loading = false;
        this._commandQ = [];
        this.setIframeDimensions = (width, height) => {
            this.iframeElement.setAttribute('width', width);
            this.iframeElement.setAttribute('height', height);
        };
        this.onWindowMessages = (e) => {
            var _a, _b, _c, _d, _e;
            if (e.source === this.iframeElement.contentWindow) {
                if (((_a = e.data) === null || _a === void 0 ? void 0 : _a.type) === _types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.READY) {
                    this.onPlayerReady();
                }
                if (((_b = e.data) === null || _b === void 0 ? void 0 : _b.type) === _types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.PLAYBACK_UPDATE) {
                    const playbackState = (_c = e.data) === null || _c === void 0 ? void 0 : _c.payload;
                    this.onPlaybackUpdate(playbackState);
                }
                if (((_d = e.data) === null || _d === void 0 ? void 0 : _d.type) === _types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR) {
                    this.emitEvent(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR, (_e = e.data) === null || _e === void 0 ? void 0 : _e.payload);
                }
            }
        };
        this.addListener = (eventName, handler) => {
            if (!this._listeners[eventName]) {
                this._listeners[eventName] = [];
            }
            this._listeners[eventName].push(handler);
            return () => {
                this.removeListener(eventName, handler);
            };
        };
        this.removeListener = (eventName, handler) => {
            if (!this._listeners[eventName] || !this._listeners[eventName].length) {
                this._listeners[eventName] = this._listeners[eventName].filter(storedHandler => handler !== storedHandler);
            }
        };
        this.on = this.addListener;
        this.off = this.removeListener;
        this.once = (eventName, handler) => {
            this.addListener(eventName, (...args) => {
                handler(...args);
                this.removeListener(eventName, handler);
            });
        };
        this.emitEvent = (eventName, eventData) => {
            var _a;
            (_a = this._listeners[eventName]) === null || _a === void 0 ? void 0 : _a.forEach(handler => handler({ data: eventData }));
        };
        this.onPlayerReady = () => {
            this.loading = false;
            this.flushCommandQ();
            this.playerReadyAck();
            this.emitEvent(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.READY, {});
        };
        this.onPlaybackUpdate = (playbackState) => {
            this.emitEvent(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.PLAYBACK_UPDATE, playbackState);
        };
        this.loadUri = (uriString, preferVideo = false, timestampInSeconds = 0) => {
            var _a;
            if (uriString !== this.currentUri) {
                const uri = (0,_spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.parseURI)(uriString);
                if (uri &&
                    (uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.EPISODE ||
                        uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.SHOW ||
                        uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.ALBUM ||
                        uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.ARTIST ||
                        uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.PLAYLIST_V2 ||
                        uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.TRACK)) {
                    this.loading = true;
                    const type = uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.PLAYLIST_V2 ? 'playlist' : uri.type;
                    const embedURL = new URL(`${this.host}/embed/${type}/${uri.id}`);
                    if (this.options.startAt || timestampInSeconds) {
                        const startAt = timestampInSeconds !== null && timestampInSeconds !== void 0 ? timestampInSeconds : parseInt((_a = this.options.startAt) !== null && _a !== void 0 ? _a : '0', 10);
                        if (!isNaN(startAt))
                            embedURL.searchParams.append('t', startAt.toString());
                    }
                    if (this.options.theme === 'dark') {
                        embedURL.searchParams.append('theme', '0');
                    }
                    if (this.queryParamReferer) {
                        embedURL.searchParams.append('referrer', this.queryParamReferer);
                    }
                    this.iframeElement.src=embedURL.href;
                    if ((uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.EPISODE || uri.type === _spotify_internal_uri__WEBPACK_IMPORTED_MODULE_0__.URITypeMap.SHOW) &&
                        preferVideo) {
                        SpotifyIframeApi.supportsVideo(uriString).then(isVideoContent => {
                            if (isVideoContent) {
                                embedURL.pathname += '/video';
                                this.iframeElement.src=embedURL.href;
                            }
                        });
                    }
                }
                else {
                    const error = {
                        code: _types__WEBPACK_IMPORTED_MODULE_2__.EmbedErrorCode.InvalidURI,
                        message: `Invalid URI: ${uriString}`,
                        recoverable: false,
                    };
                    this.emitEvent(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR, error);
                }
            }
        };
        this.playerReadyAck = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.LOAD_COMPLETE_ACK });
        };
        this.play = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.PLAY });
        };
        this.playFromStart = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.PLAY_FROM_START });
        };
        this.pause = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.PAUSE });
        };
        this.resume = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.RESUME });
        };
        this.togglePlay = () => {
            this.sendMessageToEmbed({ command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.TOGGLE_PLAY });
        };
        this.seek = (timestampInSeconds) => {
            this.sendMessageToEmbed({
                command: _types__WEBPACK_IMPORTED_MODULE_2__.IframeCommands.SEEK,
                timestamp: timestampInSeconds,
            });
        };
        this.sendMessageToEmbed = (messageToSend) => {
            if (this.loading) {
                this._commandQ.push(messageToSend);
                return;
            }
            if (this.iframeElement.contentWindow) {
                this.iframeElement.contentWindow.postMessage(messageToSend, '*');
            }
            else {
                console.error(`Spotify Embed: Failed to send message ${messageToSend}.
      Most likely this is because the iframe containing the embed player
      has not finished loading yet.`);
            }
        };
        this.flushCommandQ = () => {
            if (this._commandQ.length) {
                this._commandQ.forEach(command => {
                    setTimeout(() => {
                        this.sendMessageToEmbed(command);
                    }, 0);
                });
                this._commandQ = [];
            }
        };
        this.destroy = () => {
            var _a;
            (_a = this.iframeElement.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(this.iframeElement);
            window.removeEventListener('message', this.onWindowMessages);
        };
        this.host =
            ((_a = window.SpotifyIframeConfig) === null || _a === void 0 ? void 0 : _a.host) ||
                'https://open.spotify.com';
        this.queryParamReferer =
            ((_b = window.SpotifyIframeConfig) === null || _b === void 0 ? void 0 : _b.referrer) ||
                undefined;
        this.options = options;
        const url = new URL(this.host);
        if (!url.hostname.endsWith('.spotify.com') &&
            !url.hostname.endsWith('.spotify.net')) {
            throw new Error(`It appears that the hostname for the Spotify embed player has been overridden.
      Please make sure that "SpotifyEmbedConfig" is not being overridden.`);
        }
        const iframeElement = document.createElement('iframe');
        Object.entries(_constants__WEBPACK_IMPORTED_MODULE_1__.EMBED_REQUIRED_IFRAME_ATTRIBUTES).forEach(([attr, val]) => {
            const htmlAttr = attr.toLowerCase();
            if (typeof val === 'boolean') {
                iframeElement.setAttribute(htmlAttr, '');
            }
            else {
                iframeElement.setAttribute(htmlAttr, val);
            }
        });
        this.iframeElement = iframeElement;
        const width = (_c = options.width) !== null && _c !== void 0 ? _c : '100%';
        const height = (_d = options.height) !== null && _d !== void 0 ? _d : _constants__WEBPACK_IMPORTED_MODULE_1__.EMBED_DEFAULT_HEIGHT.toString();
        this.setIframeDimensions(width, height);
        (_e = targetElement.parentElement) === null || _e === void 0 ? void 0 : _e.replaceChild(iframeElement, targetElement);
        window.addEventListener('message', this.onWindowMessages);
    }
}
SpotifyIframeApi = {
    createController: (targetElement, options = {}, callbackOrEventsHandlers) => {
        var _a, _b, _c;
        const apiInstance = new SpotifyEmbedController(targetElement, options);
        let callback;
        let errorCallback = undefined;
        if (typeof callbackOrEventsHandlers === 'function') {
            callback = callbackOrEventsHandlers;
            if (options.uri) {
                apiInstance.loadUri(options.uri, options.preferVideo);
            }
        }
        else {
            callback = callbackOrEventsHandlers.onCreateCallback;
            if ((_a = callbackOrEventsHandlers === null || callbackOrEventsHandlers === void 0 ? void 0 : callbackOrEventsHandlers.events) === null || _a === void 0 ? void 0 : _a.onError) {
                errorCallback = callbackOrEventsHandlers.events.onError;
                apiInstance.addListener(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR, errorCallback);
            }
            if ((_b = callbackOrEventsHandlers === null || callbackOrEventsHandlers === void 0 ? void 0 : callbackOrEventsHandlers.events) === null || _b === void 0 ? void 0 : _b.onReady) {
                apiInstance.addListener(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.READY, callbackOrEventsHandlers.events.onReady);
            }
            if ((_c = callbackOrEventsHandlers === null || callbackOrEventsHandlers === void 0 ? void 0 : callbackOrEventsHandlers.events) === null || _c === void 0 ? void 0 : _c.onPlaybackUpdate) {
                apiInstance.addListener(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.PLAYBACK_UPDATE, callbackOrEventsHandlers.events.onPlaybackUpdate);
            }
        }
        if (options.uri) {
            if (!errorCallback) {
                const errorHandler = error => {
                    throw new Error(error.data.message);
                };
                apiInstance.addListener(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR, errorHandler);
                apiInstance.loadUri(options.uri, options.preferVideo);
                apiInstance.removeListener(_types__WEBPACK_IMPORTED_MODULE_2__.IframeAPIEvent.ERROR, errorHandler);
            }
            else {
                apiInstance.loadUri(options.uri, options.preferVideo);
            }
        }
        if (callback)
            callback(apiInstance);
    },
    supportsVideo: async (uri) => {
        var _a;
        const host = (_a = window.SpotifyIframeConfig) === null || _a === void 0 ? void 0 : _a.host;
        const response = await fetch(`${host}/oembed?url=${encodeURIComponent(uri)}`, {
            method: 'GET',
        });
        const data = await response.json();
        return data.type === 'video';
    },
};
/* harmony default export */ __webpack_exports__["default"] = (SpotifyIframeApi);
if (!window.onSpotifyIframeApiReady) {
    console.warn(`SpotifyIframeApi: "onSpotifyIframeApiReady" has not been defined.
  Please read the docs to see why you are seeing this warning.
  https://developer.spotify.com/documentation/embeds/references/iframe-api`);
}
else {
    window.onSpotifyIframeApiReady(SpotifyIframeApi);
}


//# sourceURL=webpack://embed-standalone-prod/./src/iframe-api/src/v1/index.ts?

 

 



 

Woof what a line of code... lol
The code you shared involves an SpotifyEmbedController managing Spotify iframe API events. The addListener and removeListener methods handle event addition and removal. Despite correct usage, the removeListener for 'playback_update' might not be working as expected. It's challenging to debug or check the source code of the Spotify API directly. Double-checking event names and their consistency could be helpful in troubleshooting.

PragueSpotify Star
Help others find this answer and click "Accept as Solution".
If you appreciate my answer, maybe give me a Like.
Note: I'm not a Spotify employee.
Marked as solution

It feels a bit janky but I got it working by redefining EmbedController._listeners['playback_update'] = []

So my working code is:

const callback = (EmbedController) => {
          const playBackPositionBroadcast = (playbackEvent) => {
   /// broadcast to dom
          };

          const removePlaybackListener = () => {
            EmbedController._listeners['playback_update'] = [];
          };
          const addPlaybackListener = () => {
            EmbedController.addListener('playback_update', playBackPositionBroadcast);
          };
    
          
          this.textAreaTarget.addEventListener('focus', removePlaybackListener);
          this.textAreaTarget.addEventListener('blur', addPlaybackListener);
          EmbedController.addListener('playback_update', playBackPositionBroadcast);
        };

Pawesome dude!

It's great to hear that you were able to make it work! Adjusting EmbedController._listeners['playback_update'] to an empty array is a workaround solution if removeListener doesn't behave as expected. While it may solve the immediate issue, directly manipulating private properties (such as _listeners) might not be the recommended approach due to potential risks related to API changes or unintended consequences.

HMU if you need me to fetch anything else,

 

-Prague the Dog

PragueSpotify Star
Help others find this answer and click "Accept as Solution".
If you appreciate my answer, maybe give me a Like.
Note: I'm not a Spotify employee.

Suggested posts