2

This is probably a simple question, but i'm totally lost.

I have this function.

m.util.genericSwipeVertFunc = function (
        ajaxRequest,
        swipeOutTarget,
        swipeInTarget
) {
    var stage1, stage2, failStage, dfd = $.Deferred(), finalStage, functionPromise;

    // Swipe of screen wait for ajax request
    stage1 = function () {
        return $.when(
            ajaxRequest, // Returns $.Deferred()
            m.util.animateDeffered(swipeOutTarget, "fadeOutDown", true) // Returns $.Deferred()
        );
    };

    // Swipe and Show
    stage2 = function () {
        swipeInTarget.show();

        return m.util.animateDeffered(swipeInTarget, "fadeInDown"); // Returns $.Deferred()
    };

    finalStage = function () {
        dfd.resolve();
    }

    failStage = function () {
        console.log("fail!");
        swipeInTarget.hide();
    };

    functionPromise = stage1()
        .then(stage2)
        .then(finalStage);

    $.when(functionPromise,dfd)
        .fail(failStage);

    return dfd;
};

Basically it does some fancy animations to fade in and out different response outputs from ajax functions. This all works fine, except when the user tries to change between targets very fast(before one chain finishes they start another) I get crazy animation all over the place.

I want to be able to reject the chain at any point by doing something like this.

// called on script load.
var currentAction = $.Deferred();

// Called everytime someone starts animation chain.
currentAction.reject();
currentAction = m.util.genericSwipeVertFunc(dfd, swipeOutTarget, swipeInTarget);
            );

With my current code the failFunction is hit correctly but it doesn't stop the execution of stage2. So it hides then shows it and continues breaking things.

So to the question. How do I put a deferred in a chain that i can reject at any time during the chains execution ? :)


Example fiddle http://jsfiddle.net/ff3jojbo/


Update for clarification

I am using animate.css for my animations. Not jquery animation. I am more interested in how to stop the chain from starting the next stage at any point from user input.


Answer fiddle http://jsfiddle.net/aefkwa8a/

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Spaceman
  • 1,319
  • 13
  • 42
  • Can create stacksnippets , jsfiddle http://jsfiddle.net to demonstrate ? Rejecting a promise returned from a function would not alone stop ongoing animations called within function before promise object returned, e.g., `swipeInTarget.show();` . Could create array of elements which would be animated , stop all ongoing animations of elements in array. – guest271314 Oct 26 '15 at 06:30
  • Stop and avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572) first! – Bergi Oct 26 '15 at 09:42
  • @guest271314 I dont really care about stopping the animations as such just the function that calls the animation. The animations can continue as long as its hidden. – Spaceman Oct 26 '15 at 22:39
  • @Bergi What part of this conforms to that pattern? The reason i created the deferred object was so i could connect it to the chain and return it not the chain. – Spaceman Oct 26 '15 at 22:57
  • @Spaceman: You could just `return functionPromise`, there's no need for that extra deferred. And why don't you want to return the chain? – Bergi Oct 26 '15 at 23:03
  • @Bergi: I want to be able to cancel the chain but the chain is a promise and doesn't have .reject as a function. So when the user starts another animation chain I can stop further execution of the current chain and call the stop/hide function. – Spaceman Oct 27 '15 at 00:15
  • Ah, I see now what you were doing there - only it doesn't work of course. That `finalStage` thingy just looked suspiciously like the deferred antipattern, and as a means of cancellation I'd rather have taken the deferred as a parameter than returned it. And you would not need `$.when(functionPromise,dfd)`, using only `dfd.fail(stopEverything)` should suffice. – Bergi Oct 27 '15 at 09:06
  • What you will need to do is to wrap the `stage2` code in a `if (!dfd.isRejected)` condition. Then it should work fine - currently it was not affected by a cancellation and just got executed anyway after `stage1()` finished. Oh, and you may want to hide (or do something else to) the `swipeOutTarget` as well, when it is cancelled during stage 1. – Bergi Oct 27 '15 at 09:08

2 Answers2

1

Try using .queue() , .promise()

// array of functions to add to queue
var arr = [];

var swipeInTarget = $("#stage1");

var swipeOutTarget = $("#stage2");

// pseudo `ajax` call
var ajaxRequest = function ajaxRequest(next) {
  return $.Deferred(function(d) {
    setTimeout(function() {
      d.resolve("ajaxRequest")
    }, Math.random() * 5000)
  }).promise()
  // Note `.then(function() {console.log(this)})` for example , 
  // can be removed
  .then(function(data) {
    console.log(data)
  }).then(next)
}

var stage1 = function stage1(next) {
  return swipeOutTarget.fadeTo(Math.random() * 5000, Math.random())
        .promise()
        // Note `.then(function() {console.log(this)})` for example , 
        // can be removed
        .then(function() {
          console.log(this)
        })
        .then(next)
}

var stage2 = function stage2(next) {
  return swipeInTarget
    .show(Math.random() * 5000, function() {
      return $(this).fadeTo(Math.random() * 2000, Math.random())
    })
    .promise()
     // Note `.then(function() {console.log(this)})` for example , 
     // can be removed
    .then(function() {
      console.log(this)
    })
    .then(next)
}
// do stuff when queue cleared
var failStage = function failStage() {
  return swipeInTarget.hide(Math.random() * 2000)
    .promise().then(function() {
      console.log("m processes stopped")
    })
}
// always do stuff when queue cleared,
// or all functions in queue complete
var finalStage = function finalStage() {
  console.log("complete", this)
}
// create jQuery object
var m = $({
  m: arr
});
// add function to `"stages"` queue
m.queue("stages", [stage1, stage2, finalStage]);
// do stuff when all functions complete , or queue cleared
m.promise("stages")
.then(finalStage);
// dequque `"stages"` queue
m.dequeue("stages");
// clear `"stages"` queue
$("button").click(function() {
  m.queue("stages", [])
  .promise("stages").always(failStage)
})
#stage2 {
  display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<button>stop m processes</button>
<div id="stage1">stage1</div>
<div id="stage2">stage2</div>
guest271314
  • 1
  • 15
  • 104
  • 177
  • 1
    This got me on the correct track see http://jsfiddle.net/aefkwa8a/ for working example – Spaceman Oct 27 '15 at 02:57
  • @Spaceman, that's way better than your original code, however it can still misbehave if you click another button while the stage2 fadeIn is happening - you sometimes end up with no section displayed. I'll see if I can find a fix later today. – Roamer-1888 Oct 27 '15 at 13:37
  • Better understanding - misbehaviour seems to occur if you click on a section as it passes over a button. You might consider adjusting the CSS such that sections pass under the buttons rather than over. – Roamer-1888 Oct 27 '15 at 20:36
1

OP's own solution here can fail after several clicks. In particular, if button is clicked while a section is flying in, then the latest demanded section may fly in, then disappear.

This solution is completely different.

Instead of using jQuery's queue/dequeue, it uses a regular stage1().then(stage2) promise chain, and stops progress down that chain by removing the CSS animation classes from the animated element and detaching its animationend handler, thus ensuring the promise associated with completion never resolves.

As you will see, much of the functionality is factored as jQuery plugins, which makes for convenient, compact syntax.

$(function () {
    // **************************
    // *** Various outer vars ***
    // **************************
    var $sections = $('#TabSection>div').hide();
    var ajaxPromise;
    var str = {
        //various strings
        'animationend': 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend',
        'fadeOutClasses': 'fadeOutDown animated',
        'fadeInClasses': 'fadeInDown animated',
        'allClasses': 'fadeOutDown fadeInDown animated'
    };

    // ***********************************************
    // *** Utilities in the form of jQuery plugins ***
    // ***********************************************
    jQuery.fn.killAnim = function(animation) {
        /* jQuery plugin :
         * Remove all the animation classes from all possible targets, and
         * detach any currently attached animationend handlers.
         * Depends on: str (object).
         */
        return this.off(str.animationend).removeClass(str.allClasses);
    };
    jQuery.fn.cssAnimate = function (animation) {
        /* jQuery plugin :
         * Perform CSS animation and return promise.
         * Depends on: str (object); killAnim (plugin).
         */
        var that = this;
        return $.Deferred(function(dfd) {
            // if no target or target not visible, resolve;
            if(that.length == 0 || !that.is(':visible')) {
                dfd.resolve();
            }
            that.addClass(animation).one(str.animationend, dfd.resolve);
        }).then(function() {
            that.killAnim();
        });
    };
    jQuery.fn.genericSwipeVertFunc = function () {
        /* jQuery plugin :
         * Sequence two CSS animations - fadeOut then fadeIn.
         * Depends on: str (object); killAnim (plugin); cssAnimate (plugin).
         */
        var that = this; // swipeInTarget
        var swipeOutTarget = $sections.filter(':visible()').eq(0);
        function stage1() {
            $sections.killAnim().not(swipeOutTarget).hide();
            return swipeOutTarget.cssAnimate(str.fadeOutClasses).then(function() {
                swipeOutTarget.hide();
            });
        };
        function stage2() {
            $sections.not(that).killAnim().hide();
            return that.show().cssAnimate(str.fadeInClasses);
        };
        return stage1().then(stage2);
    };

    // **********************
    // *** Event handlers ***
    // **********************
    $('button').on('click', function (event) {
        var inTarget = $($(this).data('tar'));
        if(ajaxPromise) {
            ajaxPromise.abort('aborted');
        }
        // *** start: emulate AJAX ***
        ajaxPromise = $.Deferred(function(dfrd) {
            setTimeout(dfrd.resolve, 1000);
        });
        ajaxPromise.abort = ajaxPromise.reject;
        // *** end: emulate AJAX ***
        ajaxPromise.then(function() {
            return inTarget.genericSwipeVertFunc();
        }).fail(function(e) {
            $sections.killAnim().hide();
            console.log(e);
        });
    });
});

I believe this solution to be more reliable. Even with lots of manic clicking, I could not defeat it.

Try it here

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44