foreach index for striping and nested use.

1,662 views
Skip to first unread message

nevf

unread,
Apr 12, 2011, 2:18:30 AM4/12/11
to KnockoutJS
I'm currently using jQuery tmpl {{each}} and have read about KO
foreach and its benefits, which I''d like to make use of. I am using
the index value passed by {each}} to do row striping as follows:


{{each(i, section) pages.sections}}
<div class={{if i%2==0}}even_section{{else}}odd_section{{/if}}
> <!-- stripe rows -->
<label>Layout</label><select data-bind="options:
ModelData.section_layouts, value: section.layout"></select>
....
<div class="common_content sub_content">Items</div>
<fieldset class="panel">
{{each(ii, item) items}} <!-- nested array:
pages.sections.items -->
<div class={{if ii%2==0}}even_common{{else}}
odd_common{{/if}}> <!-- stripe rows -->
...
{{/each}}
</fieldset>
</div>
{{/each}}

This also uses nested {{each}}'s and nested array content in the
viewmodel. Is it possible to easily replicate this using KO foreach?

-Neville

rpn

unread,
Apr 12, 2011, 10:38:20 PM4/12/11
to knock...@googlegroups.com
This is an interesting question. I would love to hear how others have solved this in a clever way.

I think that the best solution needs to work properly even when items are added or removed from the array.  Also, I think that the template for each item should not need to create a dependency on the entire array, otherwise it defeats the benefits of using foreach.

Some options that I was considering:
  • trying to use the "afterAdd", "beforeRemove", or particularly the "afterRender" callbacks on the template binding.  These always work on the DOM elements associated with the individual items in the array though, so that doesn't seem to give us any better access than what we have in the template itself.
  • calling a function that just alternates the value that it returns to alternate the style that is used.  This would work initially, but would not work when we remove an item in the middle.  Additionally, if we keep reusing this function on multiple tables, then we can't guarantee that the first row will be consistently light or dark (minor point)
  • building a special observableArray that maintains the index (as an observable) along with the value and updates the index whenever an operation is performed on the array.  Seems like it would take to much code to make this happen for too little benefit.
  • using ":nth-child(odd)" or ":nth-child(even)" CSS selectors.  This would solve the problem completely outside of Knockout, but IE doesn't support this until IE9.
Since we want to support manipulating the array (adding/removing items, etc.), it seems like the actual striped styles should not be too closely associated with items in the observableArray.  So, the way that I made this work, was with a custom binding.

The binding would be specified after the template binding like:

<ul data-bind="template: { name: 'itemsTmpl', foreach: items }, stripe: items, evenClass: 'light', oddClass: 'dark'"></ul>

The binding could look something like this:

//separate options in binding
ko.bindingHandlers.stripe {
    updatefunction(elementvalueAccessorallBindingsAccessor{
        var value ko.utils.unwrapObservable(valueAccessor())//creates the dependency
        var allBindings allBindingsAccessor();
        var even allBindings.evenClass;
        var odd allBindings.oddClass;

        //update odd rows
        $(element).children(":nth-child(odd)").addClass(odd).removeClass(even);
        //update even rows
        $(element).children(":nth-child(even)").addClass(even).removeClass(odd);;
    }
}


So, we access the observable that was passed to it (items), just to create a dependency on it and don't actually use it.  This means that whenever the items array is manipulated, the binding will run again.  The actual CSS classes to use are included in the data-bind attribute as well, and are accessed through the allbindingsAccessor.

Then, I use jQuery to make sure that the even and odd CSS classes are properly added or removed from the right children.  I suppose that you could update the rows by looping through them rather than using jQuery, but if we are using jQuery templates, then we already have a dependency on jQuery.

A couple slight alternates to this would be passing "items" and the two CSS classes as an object literal to the binding like  stripe: {watch: items, even: 'light', odd: 'dark' } or writing a custom binding that is a wrapper to the template binding, so you can specify a single binding on your element like: data-bind="templateWithStripe: { name: 'itemsTmpl', foreach: items, even: 'light', odd: 'dark' }

Here is a sample with all three of the custom bindings: http://jsfiddle.net/rniemeyer/HJ8zJ/

I am interested to see if anyone has other ideas.

nevf

unread,
Apr 13, 2011, 4:02:33 AM4/13/11
to KnockoutJS
Ryan, many thanks for your very detailed response. I am very new to KO
and am trying to keep things (code/my life) as simple as possible.
Your example does of course work, however it adds quite a bit of
complexity to something that is so simple to do with jQuery tmpl
{{each}}.

Further I don't see how this addresses my second issue "of nested
{{each}}'s and nested array content in the viewmodel.", which I assume
you missed. To clarify this, it isn't just about striping (and nested
striping), but also accessing the content of nested arrays in the
template.

-Neville

On Apr 13, 12:38 pm, rpn <rnieme...@gmail.com> wrote:
> This is an interesting question. I would love to hear how others have solved
> this in a clever way.
>
> I think that the best solution needs to work properly even when items are
> added or removed from the array.  Also, I think that the template for each
> item should not need to create a dependency on the entire array, otherwise
> it defeats the benefits of using foreach.
>
> Some options that I was considering:
>
>    - trying to use the "afterAdd", "beforeRemove", or particularly the
>    "afterRender" callbacks on the template binding.  These always work on the
>    DOM elements associated with the individual items in the array though, so
>    that doesn't seem to give us any better access than what we have in the
>    template itself.
>    - calling a function that just alternates the value that it returns to
>    alternate the style that is used.  This would work initially, but would not
>    work when we remove an item in the middle.  Additionally, if we keep reusing
>    this function on multiple tables, then we can't guarantee that the first row
>    will be consistently light or dark (minor point)
>    - building a special observableArray that maintains the index (as an
>    observable) along with the value and updates the index whenever an operation
>    is performed on the array.  Seems like it would take to much code to make
>    this happen for too little benefit.
>    - using ":nth-child(odd)" or ":nth-child(even)" CSS selectors.  This

rpn

unread,
Apr 13, 2011, 7:17:13 AM4/13/11
to knock...@googlegroups.com
I completely understand.  I don't really feel like the striping solution above is all that satisfactory.   I would love it, if  someone came in and showed us a cleaner and cleverer way to do it.

There are always advantages to keeping unnecessary complexity out of your code.  If your array is fairly small, isn't updated often, and/or your template is not expensive to render, then using {{each}} would likely not cause any problems.

As far as nested array content, I did miss that part of your question.  I assume that we are talking about accessing some of the parent's properties while in a template for a child.  This is where the 'templateOptions' functionality of the template binding comes in handy.

While in the "parent" template, we can do something like this:

<ul data-bind="template: { name: 'childTmpl', foreach: children, templateOptions: { parentItem: $data } }"></ul>

Now, we can access the "parentItem" in our child template off of $item like:

<span data-bind="text: $item.parentItem.name"></span>


One other thought: if want to use {{each}} and you have nested arrays with a child array being dynamic, then you can avoid having a dependency on everything (re-render the main and child templates on every change) by using the template binding and passing the child array in the "data" parameter.

So, in your parent template, you could do:

<ul data-bind="template: { name: 'childTmpl', data: children }"></ul>

Then, use {{each}} on the children in the template.  This would make it so your main template would not get re-rendered when this particular children observableArray is changed.  You could even pass $data into it, if you need the parent and child.

Hope this helps.

 

nevf

unread,
Apr 14, 2011, 5:46:53 PM4/14/11
to KnockoutJS
Thanks Ryan, all useful information that would likely handle my nested
array requirements.

I would like to prevent the page re-render, however I do have {{each}}
working very well and am reluctant to rework all the code at this
time. Especially seeing this is an app for internal use only.

I'll save off of this away in Surfulater for future reference. ;-)

-Neville
Reply all
Reply to author
Forward
0 new messages