Search code examples
javascriptcsstransitiontransitionend

Waiting for transitionend in a loop with JavaScript


I am so used to working with jQuery that I can't seem to figure out how I can do this in plain Javascript. I don't want to use jQuery because this is the only snippet that I use on the site and the library is too large for only that purpose.

This is the jQuery script (working): http://jsfiddle.net/1jt6dhzu/2/

$(document).ready(function () {
    var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
    var j, i = 4,
        n = whatAmI.length;

    function changeSubTitle() {
        setTimeout(function () {
            j = Math.floor(Math.random() * n - 1);
            if (j >= i) j += 1;

            $(".page-header > h2").animate({
                "opacity": 0
            }, 700, function () {
                $(this).children("span").text(whatAmI[j]);
                $(this).animate({
                    "opacity": 1
                }, 700, changeSubTitle);
            });
        }, 1000);
        i = j;
    }
    changeSubTitle();
});

And I want to swap it for vanilla JS. A large part of the loop can stay, however, the timeout and callbacks have to be replaced. I figured because I don't need IE9 support I could do this with css3 transitions and add classes. This is what I have so far:

h2 {
    opacity: 1;
    transition: opacity 700ms;
}
h2.fade-out {
    opacity: 0;
}


document.addEventListener("DOMContentLoaded", function () {
    var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
    var j, i = 4,
        n = whatAmI.length,
        heading = document.querySelector(".page-header > h2");

    function changeSubTitle() {
        setTimeout(function () {
            j = Math.floor(Math.random() * n - 1);
            if (j >= i) j += 1;

            heading.classList.add("fade-out");

            setTimeout(function () {
                heading.children("span")[0].innerHTML = whatAmI[j];
                heading.classList.remove("fade-out");
                setTimeout(changeSubTitle, 700);
            }, 700);
        }, 1000);
        i = j;
    }
    changeSubTitle();
});

Unfortunately this doesn't work. It would probably be better to swap out the timeOuts (except the most outer one) for events on transitionend. But I'm not sure how to implement this crossbrowser (IE 10 and higher, and other major browsers).


Solution

  • You had done almost everything right bar removing the children method which is implemented in a way specific to jQuery. Contrary to what I had posted earlier, JS does have a children function and it returns a HTMLCollection but it cannot filter based on element type by providing the type as param. It returns all child nodes and we must pick the correct one either by using the child element's index or by checking the type of element.

    For this example (and for simplicity sake), replace

    heading.children("span")[0].innerHTML = whatAmI[j];
    

    with

    heading.children[0].innerHTML = whatAmI[j]; // since span is the first and only child
    

    or

    heading.querySelector("span").innerHTML = whatAmI[j];
    

    and it should work as expected.

    document.addEventListener("DOMContentLoaded", function() {
      var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
      var j, i = 2,
        n = whatAmI.length,
        heading = document.querySelector(".page-header > h2");
    
      function changeSubTitle() {
        setTimeout(function() {
          j = Math.floor(Math.random() * (n - 1));
          if (j >= i) j += 1;
    
          heading.classList.add("fade-out");
    
          setTimeout(function() {
            heading.querySelector("span").innerHTML = whatAmI[j];
            heading.classList.remove("fade-out");
            setTimeout(changeSubTitle, 700);
          }, 700);
          i = j;
        }, 1000);
      }
      changeSubTitle();
    });
    h2 {
      opacity: 1;
      transition: opacity 700ms;
    }
    h2.fade-out {
      opacity: 0;
    }
    <header class="page-header">
      <h1>Bananas</h1>
    
      <h2><span>A phone</span></h2>
    
    </header>


    Using transitionend:

    transitionend is a single event listener which is attached and fired whenever the transition on an element's properties end. It will be fired both after the addition and removal of fade-out clas and so, we have to manually check what is the state of the element when the event is fired and then act based on it.

    Here I have used the getComputedStyle property to check the opacity state of the element and if it is in faded-out state then change text and remove fade-out class (or) else, call the changeSubTitle function again.

    if (window.getComputedStyle(heading).opacity == 0) {
      heading.querySelector("span").innerHTML = whatAmI[j];
      heading.classList.remove("fade-out");
    } else
      changeSubTitle();
    

    document.addEventListener("DOMContentLoaded", function() {
      var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
      var j, i = 2,
        n = whatAmI.length,
        heading = document.querySelector(".page-header > h2");
    
      function changeSubTitle() {
        setTimeout(function() {
          j = Math.floor(Math.random() * (n - 1));
          if (j >= i) j += 1;
    
          heading.classList.add("fade-out");
          i = j;
        }, 1000);
      }
      heading.addEventListener('transitionend', function() {
        if (window.getComputedStyle(heading).opacity == 0) {
          heading.querySelector("span").innerHTML = whatAmI[j];
          heading.classList.remove("fade-out");
        } else
          changeSubTitle();
      });
      changeSubTitle();
    });
    h2 {
      opacity: 1;
      transition: opacity 700ms;
    }
    h2.fade-out {
      opacity: 0;
    }
    <header class="page-header">
      <h1>Bananas</h1>
    
      <h2><span>A phone</span></h2>
    
    </header>

    Alternately (like you commented), you could check the class present on the element also and then decide accordingly. This seems a simpler method compared to my earlier one.

    if (heading.classList.contains("fade-out")) {
      heading.querySelector("span").innerHTML = whatAmI[j];
      heading.classList.remove("fade-out");
    } else
      changeSubTitle();
    

    document.addEventListener("DOMContentLoaded", function() {
      var whatAmI = ["Fruit", "Not a vegetable", "A phone", "Yummie in tummy"];
      var j, i = 2,
        n = whatAmI.length,
        heading = document.querySelector(".page-header > h2");
    
      function changeSubTitle() {
        setTimeout(function() {
          j = Math.floor(Math.random() * (n - 1));
          if (j >= i) j += 1;
    
          heading.classList.add("fade-out");
          i = j;
        }, 1000);
      }
      heading.addEventListener('transitionend', function() {
        if (heading.classList.contains("fade-out")) {
          heading.querySelector("span").innerHTML = whatAmI[j];
          heading.classList.remove("fade-out");
        } else
          changeSubTitle();
      });
      changeSubTitle();
    });
    h2 {
      opacity: 1;
      transition: opacity 700ms;
    }
    h2.fade-out {
      opacity: 0;
    }
    <header class="page-header">
      <h1>Bananas</h1>
    
      <h2><span>A phone</span></h2>
    
    </header>


    Both the above snippets have been tested in latest versions of Firefox, Chrome, Opera, IE11 and IE10 (using Emulation). It should work in latest version of Safari on Mac also. For Windows, I think Safari versions stopped with 5.1.x and still fire only webkitTransitionEnd event. To cater for these browsers, the method mentioned in this SO thread can be used.