3

I've got a table with rows generated by the foreach binding.

The only difference from the 'usual' KO tables is that for dynamic filtering of rows I'm using the visible binding on tr elements which is bound to a function that uses values of some observables to decide whether current row should be visible:

<table>
    <thead>
        ...
    </thead>
    <tbody data-bind="foreach: unfilteredItems">
        <tr data-bind="visible: $root.rowFilter($data)">
            ...
        </tr>
    </tbody>
</table>

I'm using this approach because it's a way much better in terms of performance than traditional manipulation with a collection the foreach is bound to and which results in constant inserting\removing of DOM nodes.

The only problem here is that there is no pure CSS solution for alternating the color of the rows. The tr nodes stay in the DOM and :nth-child() selector won't work properly when some trs aren't visible.

So, I'm forced to stick with the jQuery solution:

function stripeVisibleRows(tableElem) {
    var $visibleRows = $(tableElem).find('tr:visible');

    $visibleRows.filter(':odd').css('background-color', '#EEEAE7');
    $visibleRows.filter(':even').css('background-color', '#DED7D1');
};

But how can I call this function in Knockout exactly after execution of the visible bindings for all the rows when any of observables used by rowFilter triggers updating the visible bindings?

I've tried subscribeing to a fake computed depending on the rowFilter function:

self.rowFiltering = ko.computed(function () {
        return self.rowFilter();
    })
    .subscribe(function () {
        tableHelpers.stripeVisibleRows('.tbl');
    });

, writing a custom binding like this one:

// Stripes visible tr elements.
// Must be applied to a table element. 
// Must be bound to the same expression as used to change visibility of the tr elements.
ko.bindingHandlers.stripeVisibleRows = {
    update: function (element, valueAccessor) {
        // Read provided binding value to establish dependencies tracking
        ko.utils.unwrapObservable(valueAccessor());

        tableHelpers.stripeVisibleRows(element);
    },
    after: ['visible']
};

and its usage:

<table data-bind="stripeVisibleRows: $root.rowFilter()">
    <thead>
        ...
    </thead>
    <tbody data-bind="foreach: unfilteredItems">
        <tr data-bind="visible: $root.rowFilter($data)">
            ...
        </tr>
    </tbody>
</table>

but with both methods my jQuery striping function gets called before applying the visible bindings and, therefore, doesn't work properly.

Could someone advice how to achieve what I'm trying to do?
Maybe I even need to change the whole approach to filtering and striping rows, but it should be without inserting\removing DOM nodes and as clean and reusable as possible.

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
  • 1
    http://stackoverflow.com/questions/3773890/zebra-striping-a-table-with-hidden-rows-using-css3 – epascarello Sep 24 '15 at 15:52
  • @epascarello: Which answer from the link you've posted could I use to address my question? – Alexander Abakumov Sep 24 '15 at 16:14
  • Am I right in thinking that a change in `self.rowFilter()` is the cause of the updates? – James Thorpe Sep 24 '15 at 16:15
  • @AlexanderAbakumov I've amended (added a new bit really) to my answer that I believe will help – James Thorpe Sep 24 '15 at 16:20
  • @James: Yes, `self.rowFilter()` reads some `observable`s from my view model, hence when I bind the `visible` binding to the `self.rowFilter()` call, it creates the dependency of the `visible` bindings from those `observable`s and causes visibility updates when some of that `observable`'s value changed. – Alexander Abakumov Sep 24 '15 at 16:21

5 Answers5

2

You can subscribe to changes of your rowFilter, then defer execution of the call to stripeVisibleRows to ensure knockout has updated everything using the setTimeout(...,0) pattern:

self.rowFilter.subscribe(function() {
    setTimeout(function() {
        tableHelpers.stripeVisibleRows('.tbl');
    }, 0);
});

For situations where you don't mind elements being removed/added from the DOM, you can use the if binding instead of visible - this removes the markup entirely from the DOM, rather than just hiding it:

<table>
    <thead>
        ...
    </thead>
    <tbody data-bind="foreach: unfilteredItems">
        <tr data-bind="if: $root.rowFilter($data)">
            ...
        </tr>
    </tbody>
</table>
Community
  • 1
  • 1
James Thorpe
  • 31,411
  • 5
  • 72
  • 93
  • This is exactly what I'm trying to avoid: rather than inserting\removing DOM nodes I want just `display: none` them for performance reasons. See bottom note in my question. – Alexander Abakumov Sep 24 '15 at 15:59
  • @AlexanderAbakumov Ah, sorry - I did indeed miss that bit. Having said that, are you sure that doing so would be a performance hit in your case? If there's that much data being shown/hidden to make it slow, perhaps there's an alternative approach that should be taken? – James Thorpe Sep 24 '15 at 16:00
  • Yes, this is the exact performance bottleneck in my (and I believe in any web app with 100+ rows that need to be dynamically filtered). When I started to use `display: none`-like solutions, the filtering performed 10 and even more times faster than with inserting\removing DOM nodes. For this particular case, I can't use paging because of requirements. So if I could somehow managed to get working the jQuery striping with Knockout observables, that would be the best shot. – Alexander Abakumov Sep 24 '15 at 16:07
  • setTimeout() pattern is definitely does what is supposed to do here! So it's the first working KO solution so far which requires as little effort for reuse as possible! Thank you! The only thing here that I don't like is that it's not 'pure' KO. Doesn't KO provide some extensibility point which we can use to embed the desirable behavior? It seems so simple 'Call this after all bindings are aplied' that it's tough to believe there is no such a possibility... :) – Alexander Abakumov Sep 24 '15 at 17:52
2

You kind of have to do everything behind the scenes and just expose it through functions. The striping can be done with CSS classes, so you don't need to worry about using jQuery.

Since the data items don't change, they're just a non-observable array. Behind the scenes, we have a computed based on rowFilter, which accesses some observable. The foreach calls functions that look at the item in the computed that corresponds to the current item, to set visibility and class.

In this example, you're filtering the elements by their number of letters.

var tracker;

vm = {
  letters: ko.observable(3),
  unfilteredItems: ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'],
  visible: function(idx) {
    return tracker()[idx].visible;
  },
  odd: function(idx) {
    return tracker()[idx].odd;
  }
};

function rowFilter(item) {
  return item.length === vm.letters();
}

tracker = ko.pureComputed(function() {
  var visibleCount = 0;
  var result = ko.utils.arrayMap(vm.unfilteredItems, function(item) {
    var visible = rowFilter(item);
    if (visible) ++visibleCount;
    return {
      visible: visible,
      odd: visibleCount % 2
    };
  });
  return result;
});

ko.applyBindings(vm);
tr {
  background-color: lightgreen;
}
tr.odd {
  background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="value:letters, options:[3,4,5]"></select>
<table border="1">
  <tbody data-bind="foreach: unfilteredItems">
    <tr data-bind="visible: $root.visible($index()) , css: {odd:$root.odd($index())} ">
      <td data-bind="text:$data"></td>
    </tr>
  </tbody>
</table>
Roy J
  • 42,522
  • 10
  • 78
  • 102
  • I'm impressed [one more time](http://stackoverflow.com/a/32685593/3345644) your ability to elegantly bind things that, at first glance, are looking impossible to bind simply! This solution delegates all the nasty work of creating an observable for each row to the KO and looks much cleaner than the other suggestions. Thank you! Will check it out! – Alexander Abakumov Sep 27 '15 at 16:17
  • Could you please take a look at [this](http://stackoverflow.com/questions/32811802/custom-binding-that-adds-bindings-to-child-nodes) question where I'm trying to take a step further with your solution? – Alexander Abakumov Sep 27 '15 at 19:31
1

Rather than calculating all the row stripes once the table has been rendered (ie: with stripeVisibleRows) I would instead pass the current loop $index to a separate $root function, and let it determine what the current stripe is for that index. For example:

Html:

<style>
  .On { background-color: #CCC }
  .Off { background-color: #FFF }
</style>

<table id='test'>
    <thead>
        ...
    </thead>
    <tbody data-bind="foreach: unfilteredItems">
        <tr data-bind="visible: IsVisible, css: $root.rowColour($index)">
            <td data-bind='text: Text'></td>
        </tr>
    </tbody>
</table>

Javascript:

var vmItem = function(visible, text) {
    this.IsVisible = visible;
  this.Text = text;
}

var vm = {
    unfilteredItems: [
        new vmItem(true, "Some"),
        new vmItem(true, "Data"),
        new vmItem(false, "Not Shown"),
        new vmItem(true, "Shown"),
        new vmItem(true, "To"),
        new vmItem(false, "Blah!"),
        new vmItem(true, "The User")
    ],
    rowColour: function(index) {
    var toggle = true;
        for(var c = 0; c < index(); c++) {
      // if the row at this index is visible, flip the row toggle
      if (this.unfilteredItems[c].IsVisible) toggle = !toggle;
    }
    return toggle ? "On" : "Off";
    }
};

ko.applyBindings(vm, $('#test')[0]);

Example CodePen

Stumblor
  • 1,118
  • 8
  • 16
  • 1
    Nice answer, and keeps it all properly knockout based too. FWIW, I [updated your codepen](http://codepen.io/anon/pen/EVgpEy?editors=101) to make `IsVisible` observable, and after 2 seconds it shows one of the hidden rows to show that the colouring still updates correctly – James Thorpe Sep 24 '15 at 16:43
  • @Stumblor: As far as I understand, this will work only if visibility of each row is known prior to and during `foreach` is rendering the rows? I.e. if you want to change visibility of some row after rendering (exactly what dynamic filtering will be doing), color of the rows won't be recalculated and striping will be out of sync, unless you make a special observable property for each row and would need need to update these properties each time you want to change visibility? – Alexander Abakumov Sep 24 '15 at 17:30
  • @AlexanderAbakumov exactly so, which is how James Thorpe applied changes in his modified example above. This is good way of doing it for a few reasons, not least of which is that data pertaining to object composition is encapsulated within the object itself. You do have other options though - your `unfilteredItems ` array could instead be an observable array, meaning that any changes you make to it (push pop etc) would be immediately reflected in the dom, meaning you could effectively do away with needing to calculate visible rows and revert to a more simple odd/even `$index` test. – Stumblor Sep 24 '15 at 18:47
  • @Stumblor: Got it, thank you! From the pattern point of view, visibility of the row (at least in my case) isn't a state of the data object the row is representing. So why do I want to mix data and its representation? If you're adding a properties to a data objects just to support its visibility, you're likely to gain another problems - like you need to remove this property before mapping back to DTO to send it to the server. Also this method requires more work from the developer to connect things in the other places, which impacts on the reusability. This is why I prefer the custom binding. – Alexander Abakumov Sep 24 '15 at 20:16
  • @Stumblor: And another even more important impact of this approach is that if you have, say 1000 rows, you need to create 1000 extra observables and KO will need a notable amount of extra memory and extra work to maintain dependencies which potentially can nullify the effort I'm making to achieve better performance in this case. – Alexander Abakumov Sep 24 '15 at 20:17
  • @AlexanderAbakumov which is why making the array that you are iterating over observable might be the better option. Remember also that these objects we are passing to KO are ViewModels - it is their role to hold information pertaining to the dom. Whatever DTO solution you use (I use Backbone) will have no awareness of these observables. – Stumblor Sep 24 '15 at 20:33
  • @Stumblor: AFAIK, using `observableArray` won't help with hiding `tr`s via `display: none`. It can be used only for insert\remove nodes with the `foreach` binding. Or I'm missing something? – Alexander Abakumov Sep 24 '15 at 20:49
  • @Stumblor: `Remember also that these objects we are passing to KO are ViewModels - it is their role to hold information pertaining to the dom.` It's fair enough, but, in case of filtering rows, I don't want a data object to be aware of whether it is filtered out or not. I want filtering to be a totally separate concern. – Alexander Abakumov Sep 24 '15 at 20:51
  • @AlexanderAbakumov you don't even need a binding to manage the display of the rows - if they aren't part of the observable array, they won't display. See other tickets around [observable array filtering](http://stackoverflow.com/questions/20857594/knockout-filtering-on-observable-array) – Stumblor Sep 24 '15 at 20:55
  • @Stumblor: They won't display because they aren't in the DOM :) And when I need them to display - they must be inserted to the DOM. This is exactly what I'm trying to avoid - inserting\removing 100 - 1000 DOM nodes when a user changes the filter conditions which will lead to freezing browser for about 10 - 20 secs (see my initial question). So this is not an option for me in this case. – Alexander Abakumov Sep 24 '15 at 21:17
1

Using the @JamesThorpe's suggestion about setTimeout(...,0) pattern, I've managed to get the custom binding from my question to work properly:

// Stripes visible tr elements.
// Must be applied to a table element. 
// Must be bound to the same expression as used to change visibility of the tr elements.
ko.bindingHandlers.stripeVisibleRows = {
    update: function (element, valueAccessor) {
        // Read provided binding value to establish dependencies tracking
        ko.utils.unwrapObservable(valueAccessor());

        setTimeout(function () {
            tableHelpers.stripeVisibleRows(element);
        }, 0);
    }
};

function stripeVisibleRows(tableElem) {
    var $visibleRows = $(tableElem).find('tr:visible');

    $visibleRows.filter(':odd').css('background-color', '#EEEAE7');
    $visibleRows.filter(':even').css('background-color', '#DED7D1');
};

I use this binding like so:

<table data-bind="stripeVisibleRows: $root.rowFilter()">
    <thead>
        ...
    </thead>
    <tbody data-bind="foreach: unfilteredItems">
        <tr data-bind="visible: $root.rowFilter($data)">
            ...
        </tr>
    </tbody>
</table>

This is the best solution in terms of performance and reusability I've got so far to dynamically filter a table and alternate colors of its rows.

Just remember 2 things:

  1. You must bind the stripeVisibleRows binding to the same expression as you use for the trs visibility binding to keep filtering and alternating in sync. The only difference between them is that you don't pass the $data in the stripeVisibleRows binding expression.
  2. In the current implementation, you must apply the stripeVisibleRows binding to the table element.
Community
  • 1
  • 1
Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
  • 1
    Not particularly knockout-ey though :-) – Stumblor Sep 24 '15 at 18:50
  • @Stumblor: What exact part(s) of this do you suppose to be not Knockout-ey? :) – Alexander Abakumov Sep 24 '15 at 20:04
  • 1
    in my view, a pure knockout application should try to keep non-bound dom manipulation to a minimum. In the solution above, all of the row striping is done using a jquery heavy function targeting particular selectors. Since it is not bound in the html (or template) you could potentially have a maintainability issue. – Stumblor Sep 24 '15 at 20:50
  • @Stumblor: I'm not doing here something that could interfere with KO DOM manipulations. So, in my opinion it's perfectly correct to do it this way. From my experience, there is no such thing as 'pure KO app' in the real world. KO is just a framework solving the limited circle of problems using general patterns and, sometimes, giving us the possibility to fine tune them to suit our particular needs. See for example the last example from the official KO docs [here](http://knockoutjs.com/documentation/foreach-binding.html), which heavily uses jQuery selectors to perform animations. – Alexander Abakumov Sep 24 '15 at 21:10
  • 1
    @Stumblor: Throughout the official KO docs, I never seen a discourage to use jQuery where it better solve your problems. As opposite, in many official examples jQuery is used together with KO including selectors and at least simple DOM\CSS properties manipulations. – Alexander Abakumov Sep 24 '15 at 21:22
  • I agree, KO isn't meant as a replacement to JQuery, but an attempt to encapsulate (and thus, separate) data and behaviour into a ViewModel, and the UI into html/templates. To me, a heavy reliance on targetting dom elements in the JS means any attempt to modify or extend that dom structure later will be more difficult. For example, if you decided that you also wanted to support div structures in your list, you need to change both the html AND the javascript - meaning that display logic is not encapsulated. Perhaps we're splitting hairs now :-) – Stumblor Sep 25 '15 at 08:02
1

To implement a full knockout solution you'd need to expose both the visibility and the striping in the rows themselves, and have them calculated in the viewmodel.

Remember that javascript is dynamic, so you can easyly add new properties to the rows.

As you don't show your viewmodel, I'll explain how you can do it, and include a sample view model which does this:

  1. take each row, and add an observable, or computed observable (depends on your viewmodel) to control the row visibility, like this: row.visible = ko.computed(function() { /*your code here*/});
  2. in each row, add an observable for striping, like this: row.visible = ko.observable(/*your initial value*/);
  3. create a function that updates the striping for each row
  4. subscribe a new function to each row visible observable, and use it to update the striping in each row

NOTE: To avoid using loops for your calculations, you can use a library like lodash which simplifies array/collection manipulation.

You can see this solution working in this fiddle;

HTML:

<ul data-bind="foreach: rows">
    <li data-bind="text: val(), visible: visible, attr:{class: striping}">
    </li>
</ul>
Show only rows that contain:
<input type="text" data-bind="value: mustContain"/>       

Javascript (it uses lodash. See note at the bottom):

var Vm = function(_rows) {
    var self = this;
    // filter condition on view model
    self.mustContain = ko.observable('');
    // rows array
    self.rows = _rows;
    // this will update striping
    var updateStriping = function() {
        var visibleRows = _.filter(rows,function(r) {
            return r.visible();
        });
        _.forEach(visibleRows, function(r,i) {
            r.striping(i % 2 ? 'odd' : 'even');
        });
    };
    _.forEach(self.rows, function(row) {
        // make observable version of value
        row.val = ko.observable(row.value);
        // add visibility to each row
        row.visible = ko.computed(function() {
            return row.val().match(self.mustContain());
        });
        // add striping to each row
        row.striping = ko.observable('');
        // subscribe visible change
        row.visible.subscribe(updateStriping);
    });
    updateStriping();
    return self;
};

var  rows = [
    { value: 'alpha' },
    { value: 'beta' },
    { value: 'gamma' },
    { value: 'delta' },
    { value: 'epsilon' },
    { value: 'zeta' },
    { value: 'eta' },
    { value: 'theta' },
    { value: 'iota' },
    { value: 'kappa' },
    { value: 'lambda' },
    { value: 'mu' },
    { value: 'nu' },
    { value: 'xi' },
    { value: 'omicron' },
    { value: 'pi' },
    { value: 'ro' },
    { value: 'sigma' },
    { value: 'tau' },
    { value: 'upsilon' },
    { value: 'phi' },
    { value: 'chi' },
    { value: 'psi' },
    { value: 'omega' }];

var vm = new Vm(rows);

ko.applyBindings(vm);

And CSS:

.odd { background-color: silver; }
.even {}

NOTE: This code is using the attr binding for setting the striping class. You'd normaly use css instead.

NOTE on lodash and EcmaScript 5 (thank you to Alexander Abakumov's comment): most modern browsers support ES5, and it offers some of the same functionalities of lodash. In this case you can remove lodash, and change the code in this way, using the native filter and forEach:

            var visibleRows = _.filter(rows,function(r) { // Line 9
            var visibleRows = rows.filter(function(r) {

        _.forEach(visibleRows, function(r,i) { // Line 12
        visibleRows.forEach(function(r,i) {

    _.forEach(self.rows, function(row) { // Line 16
    self.rows.forEach(function(row) {

This is the fiddle with theses changes.

JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • Basically, the idea of this solution is the same as in the [@Stumblor's answer](http://stackoverflow.com/a/32766657/3345644) - we need to manually add observables for every row. Anyway, thank you for putting it as a ready-to-use exapmle! It would be useful for those that are ok with extending their data row objects with a presentation-related property. Personally, I like the [@Roy's solution](http://stackoverflow.com/a/32771534/3345644) which performs the task completely outside of the data objects. – Alexander Abakumov Sep 27 '15 at 16:35
  • By the way, both `filter` and `foreach` methods are available in ECMA5-enabled browsers as native JS methods, so in case you don't use Lodash in your project, it's better to use native methods and provide a shim for these two for older browsers rather than employing the whole new library for such a task. – Alexander Abakumov Sep 27 '15 at 16:36
  • Thank you for your comments. I missed the ES5 functionalities. I'm updating the question with that information. As to Roy's solution, it's smart, but it can be quite hard to understand by people new to JavaScript. And, although my answer is similar to Stumblor's, it's a little bit more idiomatic, IMO: I don't like leaking function calls to the bindings, because,between other things, the whole thing becomes harder to test. – JotaBe Sep 28 '15 at 08:33