31

Basically I have an observableArray and the values for each item are not an observable. This means when I change an item value the UI within a foreach loop of the observableArray does not update accordingly.

It means a massive change if I have to set them to observable, so is there a way I can refresh either the UI or observableArray foreach manually?

user1166905
  • 2,612
  • 7
  • 43
  • 75

5 Answers5

61

Yes, you can call valueHasMutated function for your array:

yourArray.valueHasMutated();

EDIT: If first didn't help you can do 'dirty' refresh:

self.refresh = function(){
    var data = self.array().slice(0);
    self.array([]);
    self.array(data);
};

Here is working fiddle: http://jsfiddle.net/vyshniakov/FuEy6/2/

Artem Vyshniakov
  • 16,355
  • 3
  • 43
  • 47
  • 9
    why do you need to make a copy of the array by calling slice(0)? the following works as well and even better `var data = self.array();self.array(null);self.array(data);` – Trident D'Gao Mar 07 '14 at 03:44
  • 1
    This dirty refresh will be expensive if the HTML generated by the array binding is big. That can happen if the array has a lot of elements, or if each elements renders a big template or chunk of HTML elements. If that's a problem for you, you can use my solution below, which is cheaper. – JotaBe Oct 29 '15 at 18:30
  • 4
    Knockout really is a POS when it comes to handling arrays. Why can't it be more like Angular - i.e. just work!? – garryp Jul 25 '16 at 15:55
21

When you have an observable array with non observable elements, and some properties of one of the elements in the array changes, if you want to refresh only that element, you can use indexOf and splice, like this:

var changedIdx = obsArray.indexOf(changedItem);
obsArray.splice(changedIdx , 1); // removes the item from the array
obsArray.splice(changedIdx , 0, changedItem); // adds it back

What this does, is looking for the element in the array, removing it, and inserting it back. When it's inserted back, the element is bound again, with the new values.

If you like this solution, you can extend the functionality of all ko observable arrays, like this:

ko.observableArray.fn.refresh = function (item) {
    var index = this['indexOf'](item);
    if (index >= 0) {
        this.splice(index, 1);
        this.splice(index, 0, item);
    }
}

Then, when you change an item of an array, you simply have to make this call:

obsArray.refresh(changedItem);

If you have many elements in your array you'll get improved performace with respect to the dirty refresh by Artem Vyshniakov, which updates the bindings for all the elements in the array, and not only for the changed one.

Note: the valueHasMutated, appart from being undocumented (and for internal use, I suppose), only checks if the array itself has changed, not if the non-observable elements in the array have changed, so it doesn't work. I.e. it only detects if you have added elements to the array, removed elements from the array, changed elements of the array with new, different elements, or changed the order of the elements. But it doesn't check if the elements themselves have changed

JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • Note that `obsArray.indexOf()` look for element just matching it value using `===` but in javascript `{'x': 'X'} === {'x': 'X'}` return false – Vitaly Zdanevich Apr 24 '17 at 14:07
  • You're right. However, in this scenario **you have a reference to the array element which you want to mofidy**. You're not modifying an object which looks like the one in the array, but the object in the array. So, using `indexOf` will find it. If you changed an object which is not in the array, you would have to replace the object in the array with the new one, which is different from changing properties of **an element in the array** and notifying that it has changed, The other case is completely different. – JotaBe Apr 24 '17 at 14:32
  • Seems quite inefficient from efficiency and memory standpoint, deleting an item from middle of array implies a copy of all item references into a new array, then inserting an item into middle of array implies another copy of all item references into a new array. – enorl76 Jun 15 '18 at 20:36
  • Unfortunately this doesnt work with rateLimit applied to the array if you did a remove and push with subscribing to an arrayChange – Mike Flynn Aug 10 '22 at 19:43
5

I ended up using the dirty approach from above but made it so that all my observable arrays had this functionality.

ko.observableArray.fn.refresh = function () {
        var data = this().slice(0);
        this([]);
        this(data);
    };

The valueHasMutated didn't work for me. Its kind of lame the entire list has to be updated. Or in my case find a way to extend the ko mapping plugin to fill the observable arrays with observable objects..

MrB
  • 1,544
  • 3
  • 18
  • 30
0

I found a simple solution by replacing the entire object in the array.

In this example parameter ix is the index, oLine is the updated object and oVM.objProps.lines contains the array. The solution worked like a champ.

/* This contortion causes the computed function to fire because
 * we have replaced the entire line object.
 */
function forceRefresh(ix,oLine) {
  var oVM = pme.getVM();
  oLine = JSON.parse(JSON.stringify(oLine));
  oVM.oObjProp.lines[ix] = oLine;
}
Steve Pritchard
  • 207
  • 3
  • 5
0

I'm using Knockout with deferUpdates and the solution of JotaBe needs an update.

It seems Knockout detects, on remove/add, that is the same item and so don't refresh the array.

I have adopted the following solution:

ko.observableArray.fn.refresh = function (item, index) {
    if (index==null) index = this['indexOf'](item);
    if (index >= 0) {
        this.splice(index, 1, ko.utils.extend({}, item)) // create new item
        //this.splice(index, 1);
        //this.splice(index, 0, item);
    }
}

and it refresh the array correctly! :-)

Note I added a second argument to refresh function, for specifying index when index is given, for avoiding indexOf run.

RFex
  • 94
  • 1
  • 2
  • Would this work with a complex object with many properties and other arrays? I have ratelimit and arrayChange doesnt get hit for the array subscription when removing/pushing same item. – Mike Flynn Aug 10 '22 at 19:45