1
votes

Ok, the title may be a bit confusing so I'll explain...

I'm writing a custom binding to which I'll pass an observableArray. This observableArray is populated asynchronously and the elements are pushed in one by one.

The problem is that my custom binding is called (the update method) each time the observableArray is mutated. Which makes sense but which isn't helpful in this instance because it means that the first element gets rendered n times, where n is the length of the observableArray, and the second element gets rendered n-1 times, with only the nth element being rendered once.

Can anyone explain a neat method of having the custom binding only do something when either

  • the observableArray is fully populated, or
  • when an element has been added which has not already been rendered by the custom binding?

I can think of a couple of ways around this using an additional property/observable as a flag on the parent view-model which says "fully populated, you can render the items now" or a property on each of the elements which says "you've already rendered me". However, these are both awkward, particularly as the objects inside the observableArray also have an observableArray property.

Isn't there a better Knockout/MVVM solution to this problem?

UPDATE: For clarity, what I'm building on top of is something like this

<domElmnt data-bind="myBinding: { collection: TypeGroups }" />

where

TypeGroups = ko.observableArray();

and where the elements contained in TypeGroups are all instances of another view-model with observable properties.

Each time I call TypeGroups.push(obj) the custom binding is called once again.

3
The observableArray shouldn't rerender elements that didn't change. Try adding a binding that display new Date() to check if it actually rerender things. Something like <span data-bind="text: new Date()"></span> - Loïc Faure-Lacroix
The custom binding is on the observableArray or the items? - Loïc Faure-Lacroix
The observableArray isn't re-rendering; what's happening is that each time the observableArray is mutated it calls the custom binding, which then loops through all items in the observableArray - even those it's already processed. - awj
Is there a reason you can't use the foreach binding? - Michael Best
@Michael - yes: because once inside the loop, prior to any other markup, is another loop, and IE8/9 strips out the containerless binding that this nested loop needs. We did have a foreach in place until we found IE9 was broken, and that's the very reason that I've moved to a custom binding. - awj

3 Answers

2
votes

Can't tell exactly if it's necessary but here is how I'd do it if the customBinding has to be put on the observableArray.

What you want to do is an atomic update. There is the method splice just like the usual array splice.

yourArray.splice(atIndex, 0, elem1, elem2, elem3 ...)

But like that it's not really useful:

yourArray.splice.apply(yourArray, [index, 0, elem1, elem2, elem3 ...])

It's not really pretty like that but we can do better.

function append(array, values) {
    var params = [array().length, 0];
    params.push.apply(params, values);
    array.splice.apply(array, params);
}

I'm pretty sure that using splice will append all elements atomatically without calling update multiple times. On the other hand, calling array.push.apply(array, values) might be enough and splice isn't needed.

But!

If I were in your position, I'd move the binding from the obserableArray to the items inside the observable array. I don't know why you are iterating over all your items after update. If elements were processed once, is there any reason to process processed items multiple times? If not then the custom binding might not be used where it should.

Without knowing more about your problem, it's hard to say what is the best way to handle your problem.

0
votes

Look into ko.utils.arrayPushAll().

You dump all the objects into an array and push them all with one transaction. This will updat the UI once.

Check out this article by Ryan Niemeyer Utility Functions

0
votes

Here's how I solved this problem: each time the custom binding is called I (safely) count how many children have already been added to the context element:

var childCount = 0,
    children = element.childNodes,
    childMax = children.length;
for (var c = 0; c < childMax; c++) {
    if (children[c].nodeType != 3) {
        childCount++;
    }
}

Then, when I loop through the specified observableArray I use childCount - which starts at the previous-element-count - as the starting index, and loop from that index onwards:

var coll = groupsCollection();    // groupsCollection is the specified observableArray
childMax = coll.length;
for (; childCount < childMax; childCount++) {
    // do something with coll[childCount ], such as using it to add a DOM node
}