Search code examples
javascriptjqueryknockout.jscomputed-observable

Animated transition beforeRemove/afterAdd when foreach with computed observable


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);

Solution

  • 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();