user2465510 user2465510 - 1 month ago 6
HTML Question

Recursive function to display fade in/out text in Javascript with JQuery

I am trying to create a function speak() which recursively runs through an array of Strings, fades each one in and out, and then proceeds to the next string. Here is the code I have:

function speak(num, arr, length, time)
{
if(num < length)
{
var string = arr[num];
$("#maintext").text(string);
$("#maintext").fadeIn(time, function ()
{
$("#maintext").fadeOut(time, speak(num+1, arr, length, time));
});
}
}


The problem I am having is that the string fades in, switches to the next string, then fades out. I want it to fade in, fade out, and then switch to the next string while #maintext is opaque. No matter what I try with this function, I can't figure out what is going wrong.

It prints each string, the transition is just in the wrong place.

JFiddle, although I couldn't get the JFiddle to work at all (I think I probably formatted it wrong).
https://jsfiddle.net/t2q9jdsx/10/

Answer

This should help you. Instead of setting the .text first, I fade out the element, then swap the text, then fade it back in. You'll see it work when you run the code snippet below. It's also quite handy that it uses the text content provided to the #maintext element as the initial text. You'll also see that the last string is the one that remains displayed once we've finished recursing thru the array.

Using your code, the #maintext would always finish with a fadeOut which means that #maintext will always be invisible after displaying all the strings.

const speak = ($elem, time, [x,...xs]) => {
  if (x === undefined) return null;
  $elem.fadeOut(time, () => {
    $elem.text(x).fadeIn(time, () => {
      speak($elem, time, xs)
    })
  })
}

let strings = ['foo', 'bar', 'baz', 'bof']

speak($('#maintext'), 1000, strings)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="maintext">init</div>

If you're not transpiling your JS with babel, you might want the pre-ES6 version. Here you go

function speak($elem, time, strings) {
  if (strings[0] === undefined) return null;
  $elem.fadeOut(time, function() {
    $elem.text(x).fadeIn(time, function() {
      speak($elem, time, strings.slice(1))
    });
  });
}

Other remarks

You were on the right track using a state variable to keep track of the index in your recursive function. However, I generally find it easier to just slice off the head (first element) of the array and recurse using the remaining elements. To show you that your approach was equally viable, I'll re-implement my answer using an index instead.

The biggest difference here is that we now need 4 variables in our function (instead of 3), and we need to be comparing the index against strings.length for each iteration. It's not a huge issue, but the extra cognitive overhead of keeping track of an index and constantly checking array length is the reason I prefer the form/style of the first answer I gave.

function speak ($elem, time, strings, i) {
  if (i >= strings.length) return;
  $elem.fadeOut(time, function () {
    $elem.text(strings[i]).fadeIn(time, function () {
      speak($elem, time, strings, i + 1);
    });
  });
}

var strings = ['foo', 'bar', 'baz', 'bof'];

speak($('#maintext'), 1000, strings, 0);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="maintext">init</div>

Goal: jQuery plugin

jQuery makes it insanely easy to make plugins. As soon as you start using a jQuery method like .text or .fadeOut inside of one of your functions, you should be asking yourself if a jQuery plugin is a better fit.

// non-plugin
speak($('#maintext', someStrings))

// plugin
$('#maintext').speak(someStrings)

I'm pretty sure you'll agree with me that the second option is much better in this case. Let me show you how easy it is to do that.

($ => {
  // this function doesn't have to change at all
  const speak = ($elem, time, [x,...xs]) => {
    if (x === undefined) return null;
    $elem.fadeOut(time, () => {
      $elem.text(x).fadeIn(time, () => {
        speak($elem, time, xs)
      })
    })
  }
  // create the plugin; sensible default value for `time`
  $.fn.speak = function(strings, time=1000) {
    return $(this).each((idx, elem) =>
      speak($(elem), time, strings));
  };
}) (jQuery);


$('#maintext').speak(['the', 'end', 'of', 'the world']);
$('#othertext').speak(['la', 'fin', 'du', 'monde']);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="maintext">init</div>
<div id="othertext">init</div>

Now it works, seamlessly with multiple elements on the page and can even be run on elements simultaneously