2

I am facing some challenges trying to use jquery animations such as fadeIn() fadeOut() with knockout.

Live example, without animation: http://jsfiddle.net/LkqTU/23801/

I use a computed observable to filter my original array of charities. The computed is data-bound with a foreach and I want to make the entire container (with class .tab) fade out before any changes and fadeIn after the changes.

I have tried using the built in beforeRemove and afterAdd properties, but this does not seem to work when my array is computed. As seen in the live example below, the container gets filled up by several instances of some charities, even though the underlying computed array only contains the correct ones.

Live example, with (failed) animation: http://jsfiddle.net/fy7au6x6/1/

Any suggestions on how I can control the timing of changes to the computed with the animations?

These are the two arrays, "All charities" and "Charities filtered by category":

self.allCharities = ko.observableArray([
    new Charity(0, "Amnesty International", "$2,466", "HUMANITARIAN"),
    new Charity(1, "Richard Dawkins Foundation", "$0", "EDUCATION"),
    new Charity(2, "Khaaaan Academy", "13,859", "EDUCATION"),
    new Charity(4, "Wikipedia", "$7,239",  "EDUCATION")
]);

self.filteredCharities = ko.computed(function () {

    // If no category is selected, return all charities
    if (!self.selectedCategory())
        return self.allCharities();

    // Return charities in the selected category
    return ko.utils.arrayFilter(self.allCharities(), function (c) {
        return (c.Category() == self.selectedCategory());
    });

}, this);
isherwood
  • 58,414
  • 16
  • 114
  • 157
jonasjuss
  • 173
  • 1
  • 2
  • 10
  • Did you get the wrong fiddle on the second one - it looks the same as the non-animated version, ie there's no `beforeRemove` or `afterAdd` etc – James Thorpe Apr 19 '15 at 14:31
  • 1
    [Is this](http://jsfiddle.net/LkqTU/23803/) more like what you're after? I've added in the bindings from the [knockout example](http://knockoutjs.com/examples/animatedTransitions.html) – James Thorpe Apr 19 '15 at 14:37
  • @JamesThorpe You're right, changed the failed-animation-jsfiddle now! It is not exactly what I am looking for. I would like the entire tab-container to fade out, then fade in with the updated content. – jonasjuss Apr 19 '15 at 14:40
  • 1
    Here's an example where the tab content fades in and out: http://jsfiddle.net/fy7au6x6/2/ – lagerone Apr 19 '15 at 19:25
  • @lagerone Thanks, that is the look I am going for! Only downside is that I need to make a function call to set the filteredCharities, instead of having the observable do it automatically upon changes. But I guess it is easier to control animations etc this way ;) – jonasjuss Apr 20 '15 at 06:35

2 Answers2

2

Contrary to solutions given in comments to your question, I suggest that you do not mix DOM-handling and ViewModel functionality in the viewmodel methods. Generally, I suggest avoiding doing anything which makes the viewmodel depending on the DOM.

When it comes to animations for the foreach-binding I would first recommend creating a custom bindingHandler which would actually use the foreach-binding and add the animations you want. This way, you can keep the DOM-related code in the view or the bindingHandlers, where they are supposed to be.

In some scenarios you might not want to create a custom binding for it, but just want the animation methods available to your foreach binding. In these cases, putting these methods on a viewmodel could be a more pragmatic approach. However, if you do this, I would recommend avoiding having your viewmodel functionality depending on these methods at all, just keep them only for doing the DOM animation logic.

Given such an approach, your viewmodel could then look similar to (copying the one in your fiddle and then adding the animation methods):

function ViewModel() {
    var self = this;
    self.selectedCategory = ko.observable("");

    self.setCategory = function (newCat) {
        self.selectedCategory(newCat);
    };

    self.allCharities = ko.observableArray([
        new Charity(0, "Amnesty International", "$2,466", "HUMANITARIAN"),
        new Charity(1, "Richard Dawkins Foundation", "$0", "EDUCATION"),
        new Charity(2, "Khaaaan Academy", "13,859", "EDUCATION"),
        new Charity(4, "Wikipedia", "$7,239",  "EDUCATION")
    ]);

    self.filteredCharities = ko.computed(function () {

        // If no category is selected, return all charities
        if (!self.selectedCategory())
            return self.allCharities();

        // Return charities in the selected category
        return ko.utils.arrayFilter(self.allCharities(), function (c) {
            return (c.Category() == self.selectedCategory());
        });

    }, this);

    var fadeAnimationDuration = 500;
    self.animationAfterAddingCharityElementsToDom = function(element){
        //Since this method will be depending on the DOM, avoid having 
        //the viewmodel functionality depending on this method

        //First hide the new element
        var $categoryDomElement = $(element);
        $categoryDomElement.hide();
        var $tabDomElement = $categoryDomElement.parent();
        $tabDomElement.fadeOut(fadeAnimationDuration, function(){
            //When the tab has faded out, show the new element and then fade the tab back in
            $categoryDomElement.show();
            $tabDomElement.fadeIn(fadeAnimationDuration);
        });
    };
    self.animationBeforeRemovingCharityElementsFromDom = function(element){
        //Since this method will be depending on the DOM, avoid having 
        //the viewmodel functionality depending on this method

        var $categoryDomElement = $(element);
        var $tabDomElement = $categoryDomElement.parent();
        $tabDomElement.fadeOut(fadeAnimationDuration, function(){
            //When the tab has faded out, remove the element and then fade the tab back in
            $categoryDomElement.remove();
            $tabDomElement.fadeIn(fadeAnimationDuration);
        });
    };
};

And your binding would then be:

<div class="tab" data-bind="foreach: { data: filteredCharities, afterAdd: animationAfterAddingCharityElementsToDom, beforeRemove: animationBeforeRemovingCharityElementsFromDom }">
    <div class="tab-tile mb21" data-bind="css:{'mr21':$index()%3 < 2}">
        <a href="#" class="amount" data-bind="text: Amount"></a>
        <a href="#" class="title" data-bind="text: Name"></a>
        <a href="#" class="category" data-bind="text: Category"></a>
    </div>
</div>

I have updated your fiddle with the above code and you can find it at http://jsfiddle.net/LkqTU/23825/.

It could also be a good idea (and more "correct") to add these methods to the viewmodel constructor prototype instead, if you expect that you're going to create more than one instance.

Robert Westerlund
  • 4,750
  • 1
  • 20
  • 32
1

Here's a more clean answer using a custom binding handler.

The trick is to use one boolean that says, essentially, "I am about to change"... when we set that to true, we fade out with our simple binding handler.

Once the filter is processed and ready, we set that same boolean to false which says, in essence, "I am done"... our little handler fades back in when that happens.

The trick is to use a subscription and a second observable array instead of a computed. This allows you to set the boolean to true, fill the secondary observable, then set that observable to false... this can drive the fade-in, fade-out behavior without having to worry about the timing of the binding behavior.

Fiddle:

http://jsfiddle.net/brettwgreen/h9m5wb8k/

HTML:

<div class="side-bar">
    <a href="#" class="category" data-bind="click: function(){ setCategory('')}">All</a>
    <a href="#" class="category" data-bind="click: function(){ setCategory('EDUCATION')}">Education</a>
    <a href="#" class="category" data-bind="click: function(){ setCategory('HUMANITARIAN')}">Humanitarian</a>
</div>

<div class="tab" data-bind="fader: filtering, foreach: filteredCharities">
    <div class="tab-tile mb21" data-bind="css:{'mr21':$index()%3 < 2}">
        <a href="#" class="amount" data-bind="text: Amount"></a>
        <a href="#" class="title" data-bind="text: Name"></a>
        <a href="#" class="category" data-bind="text: Category"></a>
    </div>
</div>

JS:

ko.bindingHandlers.fader = {
    update: function(element, valueAccessor) {
        var obs = valueAccessor();
        var val = ko.unwrap(obs);
        if (val) {
            $(element).fadeOut(500);
        }
        else
        {
            $(element).fadeIn(500);
        }
    }
};

function Charity(id, name, amount, category) {
    var self = this;
    self.Id = ko.observable(id);
    self.Name = ko.observable(name);
    self.Amount = ko.observable(amount);
    self.Category = ko.observable(category);
}

// ----------------------------------------------------------
// VIEWMODEL ------------------------------------------------
// ----------------------------------------------------------
function ViewModel() {
    var self = this;
    self.selectedCategory = ko.observable("");

    self.filtering = ko.observable(false);
    self.setCategory = function (newCat) {
        self.filtering(true);
        window.setTimeout(function() {self.selectedCategory(newCat);}, 500);
    };

    self.allCharities = ko.observableArray([
        new Charity(0, "Amnesty International", "$2,466", "HUMANITARIAN"),
        new Charity(1, "Richard Dawkins Foundation", "$0", "EDUCATION"),
        new Charity(2, "Khaaaan Academy", "13,859", "EDUCATION"),
        new Charity(4, "Wikipedia", "$7,239",  "EDUCATION")
    ]);
    self.filteredCharities = ko.observableArray(self.allCharities());

    self.selectedCategory.subscribe(function(newValue) {
        self.filtering(true);
        console.log(newValue);
        if (!newValue)
            self.filteredCharities(self.allCharities());
        else {
            var fChars = ko.utils.arrayFilter(self.allCharities(), function (c) {
                return (c.Category() === newValue);
            });
            self.filteredCharities(fChars);
        };
        self.filtering(false);
    });

};

// ----------------------------------------------------------
// DOCUMENT READY FUNCTION ----------------------------------
// ----------------------------------------------------------

$(document).ready(function () {

    ko.applyBindings(vm);
});

var vm = new ViewModel();
Brett Green
  • 3,535
  • 1
  • 22
  • 29