Search code examples
meteorvimeo-apiflow-router

Removing a window event listener in Template.onDestroyed


I have two templates that each contain a Vimeo iframe player. I'm using FlowRouter to render the templates through {{> Template.dynamic template=main}} on the main layout.

In both templates I add listeners for video events in onCreated

Template.view.onCreated( function() {
    var self = this;

    if (window.addEventListener) {
        window.addEventListener('message', function(event) {
            viewMessageReceived(event, self)}, false);
    } else {
        window.attachEvent('onmessage', function(event){
            viewMessageReceived(event, self)}, false);
    }
});

and destroy them in onDestroyed

Template.view.onDestroyed( function() {
    if (window.removeEventListener) {
        console.log('view remove');
        window.removeEventListener('message', function(event) {
            viewMessageReceived(event, self)}, false);
    } else {
        window.detachEvent('onmessage', function(event){
            viewMessageReceived(event, self)}, false);
    }
});

And here is the function being called by the anonymous event handler:

function viewMessageReceived(event, self) {
// Handle messages from the vimeo player only
    if (!(/^https?:\/\/player.vimeo.com/).test(event.origin)) {
        return false;
    }

    if (self.playerOrigin === '*') {
        self.playerOrigin = event.origin;
    }

    var data = JSON.parse(event.data);

    switch (data.event) {
        case "ready":
            initializePlayer(self);
            break;
        case "playProgress":
            self.playerTime.set(data.data.seconds);
            if (self.duration === '*') self.duration = data.data.duration;
            break;
        case "play":
            self.playerStatus.set("playing");
            break;
        case "pause":
            self.playerStatus.set("paused");
            break;
    }
}

When I switch to a different template, onDestroyed runs and my console.log('view remove') fires, as expected.

But then when I navigate to the page that loads the other template with a video player, a Vimeo "playProgress" message arrives that is received by the event handler in the previous video template, which was supposed to have been removed a while ago. This throws an error because the previous template has been destroyed.

Uncaught TypeError: Cannot read property 'contentWindow' of undefined

which comes from the last line in this function:

function post(template, action, value) {
    console.log('view action: %s value: %s', action, value);
    var data = {method: action};
    if (value) data.value = value;
    var message = JSON.stringify(data);
    template.player[0].contentWindow.postMessage(message, template.playerOrigin);
}

Each of the video-containing templates have their own .js file, so they each have a their own post function declaration. My understanding is that defining a function that way scopes the function just to the page.

It's only one message that arrives for the wrong player. After that, they arrive for the currently loaded player.

Why does the Vimeo message event arrive or get handled after I've already destroyed the template and when I've moved to another player?


Solution

  • A quote from the W3Schools website regarding the removeEventListener() method:

    // Attach an event handler to <div>
    document.getElementById("myDIV").addEventListener("mousemove", myFunction);
    
    // Remove the event handler from <div>
    document.getElementById("myDIV").removeEventListener("mousemove", myFunction);
    

    Note: To remove event handlers, the function specified with the addEventListener() method must be an external function, like in the example above (myFunction).

    Anonymous functions, like "element.removeEventListener("event", function(){ myScript });" will not work.

    For this to work, you'll need to move your function definition somewhere outside of the onRendered and onDestroyed events, and just pass the function name in the add/remove event listeners.