Wait till element exists

I have a script that adds a video tag to a page. That bit works fine, but having added the video I want to add an event listener to the video tag, but it comes back as undefined. I’m assuming it’s because the additional HTML hasn’t yet been added. How do I wait till the video element exists?

const pv = document.querySelectorAll(".playVid"),
  ib = document.getElementById("vidbox");
for (let j = 0; j < pv.length; j++) {
  pv[j].addEventListener("click", function() {
    let vid = this.dataset.vid;
    fetch("getVideo.php?id=" + vid)
      .then((response) => {
        if (response.ok) return response;
        throw Error(response.statusText);
      })
      .then((response) => response.text())
      .then((text) => ib.innerHTML = text)
      .catch((error) => console.log("There was an error:", error));
    document.getElementById("infobox").className = "visible";
    document.getElementById("boxHolder").style.display = "flex";
    const v = document.getElementsByTagName("video")[0];
    console.log(v); // undefined
  })
};

There’s
MutationObserver.observe() - Web APIs | MDN

It’s been a few years since I worked with it. At the time, with a good chance it was my inexperience, I found it to result in extremely heavy resource consumption. To the point that I abandoned the attempt.

That the API still exists suggests my problems were mine own fault, or perhaps things have improved since then.

1 Like

Hey @Gandalf, using a mutation observer would be an option, but if you have direct control over the video being added to the document, couldn’t you just add the event listener inside the appropriate callback? I.e.

fetch("getVideo.php?id=" + vid)
  //...
  .then((text) => {
    ib.innerHTML = text
    const video = ib.querySelector('video')
    video.addEventListener(/* ... */)
  })
2 Likes

Thanks guys. I did play with MutationObserver but got thoroughly confused.

Adding the event listener inside the callback was easy enough but it only seems to call the function once at the beginning, so I guess I’m missing something else.

const pv = document.querySelectorAll(".playVid"),
  ib = document.getElementById("vidbox");
for (let j = 0; j < pv.length; j++) {
  pv[j].addEventListener("click", function() {
    let vid = this.dataset.vid;
    fetch("getVideo.php?id=" + vid)
      .then((response) => {
        if (response.ok) return response;
        throw Error(response.statusText);
      })
      .then((response) => response.text())
      .then((text) => {
        ib.innerHTML = text;
        const v = ib.querySelector("video");
        v.addEventListener("timeupdate", updateVc(v, vid));
      })
      .catch((error) => console.log("There was an error:", error));
    document.getElementById("infobox").className = "visible";
    document.getElementById("boxHolder").style.display = "flex";
  })
};
function updateVc (v, vid) {
  console.log(v.currentTime);
  if (v.currentTime > (v.duration * .5)) {
    fetch("updVideoCount.php?id=" + vid);
    // ensure it's updated only once
    v.removeEventListener("timeupdate", updateVc);
  }
};

You need to pass a callback function to addEventListener(), but what you’re doing is calling the function directly and passing the result (which is undefined). So you’d either have to define the function in place, or return another function from updateVc():

fetch("getVideo.php?id=" + vid)
  //...
  .then((text) => {
    ib.innerHTML = text
    const video = ib.querySelector('video')

    video.addEventListener(function updateVc () {
      console.log(video.currentTime, vid)
      // ...
    })
  })

// Or:
fetch("getVideo.php?id=" + vid)
  //...
  .then((text) => {
    ib.innerHTML = text
    const video = ib.querySelector('video')
    video.addEventListener(updateVc(video, vid))
  })

function updateVc (video, vid) {
  return function () {
    console.log(video.currentTime, vid)
    // ...
  }
}
1 Like

BTW you actually only need to pass the vid variable, the video element would also be available via the event.target:

function updateVc (vid) {
  return function (event) {
    console.log(event.target.currentTime, vid)
    // ...
  }
}

Thanks again @m3g4p0p.

That gets me a step forward. I think I need to use the second option because I need to remove the event listener and I don’t think I can do that with an anonymous function? However, the removeEventListener isn’t working now.

(I haven’t got as far as implementing post #6 yet!)

But the callback isn’t anonymous in the 1st option, so you can still remove the event listener as usual:

video.addEventListener('timeupdate', function updateVc () {
  // ...
  video.removeEventLister('timeupdate', updateVc)
})

As for the 2nd option, the name of the function is probably a bit unfortunate as it’s actually a factory function; what you’d remove is the inner one that’s getting returned:

video.addEventListener('timeupdate', createUpdateHandler(vid))

function createUpdateHandler (vid) {
  return function updateVc (event) {
    // ...
    event.target.removeEventLister('timeupdate', updateVc)
  }
}
1 Like

Yey - I’ve got it working at last. thanks @m3g4p0p. :slight_smile:

2 Likes

Happy to help. :-)

1 Like