I'm trying to force a UI update on modifying a value of an object in an observableArray
, but the objects in the array aren't observable
. I assumed you could do this with valueHasMutated
, but it isn't working as I expected.
In this example if I click the update button nothing happens, but if I manually reset the array it updates:
<div id="bindings">
<ul data-bind="foreach: observableThings">
<li data-bind="text: id"></li>
</ul>
</div>
<button data-bind="click: updateValue">Update</button>
<button data-bind="click: forceUpdate">Force Update</button>
var things = [
{ id: 1, thing: false },
{ id: 2, thing: false },
{ id: 3, thing: false },
{ id: 4, thing: false }
]
var viewModel = function() {
var self = this;
self.observableThings = ko.observableArray(things);
self.updateValue = function() {
self.observableThings()[0].id = 5;
self.observableThings.valueHasMutated();
}
self.forceUpdate = function() {
self.observableThings()[0].id = 5;
var origThings = self.observableThings();
self.observableThings(null);
self.observableThings(origThings);
}
}
ko.applyBindings(new viewModel());
How is valueHasMutated
supposed to work?
I'm trying to force a UI update on modifying a value of an object in an observableArray
, but the objects in the array aren't observable
. I assumed you could do this with valueHasMutated
, but it isn't working as I expected.
In this example if I click the update button nothing happens, but if I manually reset the array it updates:
<div id="bindings">
<ul data-bind="foreach: observableThings">
<li data-bind="text: id"></li>
</ul>
</div>
<button data-bind="click: updateValue">Update</button>
<button data-bind="click: forceUpdate">Force Update</button>
var things = [
{ id: 1, thing: false },
{ id: 2, thing: false },
{ id: 3, thing: false },
{ id: 4, thing: false }
]
var viewModel = function() {
var self = this;
self.observableThings = ko.observableArray(things);
self.updateValue = function() {
self.observableThings()[0].id = 5;
self.observableThings.valueHasMutated();
}
self.forceUpdate = function() {
self.observableThings()[0].id = 5;
var origThings = self.observableThings();
self.observableThings(null);
self.observableThings(origThings);
}
}
ko.applyBindings(new viewModel());
How is valueHasMutated
supposed to work?
3 Answers
Reset to default 6Sticking a bunch of plain objects into an observable array does not magically make the properties of those objects observable.
An observable array generally only observes item removal and item insertion. If the items themselves have properties you want to observe then these properties explicitly need to be made observable.
The mapping plugin can help with that. It can do a few very nice things, do read the documentation page.
function ListOfThings(params) {
var self = this;
self.things = ko.observableArray();
self.updateValue = function() {
ko.utils.arrayForEach(self.things(), function (thing) {
thing.id( ~~(Math.random() * 100) );
});
}
// init
ko.mapping.fromJS(params, {}, self);
}
var vm = new ListOfThings({
things: [
{ id: 1, thing: false },
{ id: 2, thing: false },
{ id: 3, thing: false },
{ id: 4, thing: false }
]
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare./ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div id="bindings">
<ul data-bind="foreach: things">
<li data-bind="text: id"></li>
</ul>
</div>
<button data-bind="click: updateValue">Update</button>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
EDIT You seem to be disproportionally fixed on valueHasMutated
and the impossibility of using the mapping plugin. The mapping plugin is helpful, but it's certainly not necessary.
function Thing(params) {
this.id = ko.observable(params.id);
this.thing = ko.observable(params.thing);
}
function ListOfThings(params) {
var self = this;
self.things = ko.observableArray();
self.updateValue = function() {
ko.utils.arrayForEach(self.things(), function (thing) {
thing.id( ~~(Math.random() * 100) );
});
}
// init
self.things(ko.utils.arrayMap(params.things, function (obj) {
return new Thing(obj);
}));
}
var vm = new ListOfThings({
things: [
{ id: 1, thing: false },
{ id: 2, thing: false },
{ id: 3, thing: false },
{ id: 4, thing: false }
]
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare./ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div id="bindings">
<ul data-bind="foreach: things">
<li data-bind="text: id"></li>
</ul>
</div>
<button data-bind="click: updateValue">Update</button>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
KnockoutJS code (thanks to debugger version in your fiddle), executing on valueHasMutated leads to
evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) {
// some other code
// ...
// newValue is the same as state.latestValue => "putedObservable["notifySubscribers"]" is not called
if (putedObservable.isDifferent(state.latestValue, newValue)) {
if (!state.isSleeping) {
putedObservable["notifySubscribers"](state.latestValue, "beforeChange");
}
state.latestValue = newValue;
if (state.isSleeping) {
putedObservable.updateVersion();
} else if (notifyChange) {
putedObservable["notifySubscribers"](state.latestValue);
}
}
This can do the trick:
self.updateValue = function() {
var index = 0;
var item = self.observableThings()[index];
// Update item content
item.id = 5;
// Force array to update markup
self.observableThings.splice(index, 1);
self.observableThings.splice(index, 0, item);
}
But IMHO the better way is to use observable properties.
valueHasMutated
would typically be used if you wanted to change the contents of a large observableArray multiple times but only trigger a single change notification and a single UI update or whatever. So you would operate on the underlying array and then call valueHasMutated
when all your changes are plete.
The reason that your call to valueHasMutated
isn't doing anything is because the 'value' of the observable array (meaning the items in it, not the properties of those items) has not changed. Your call causes a re-evaluation of the state of the array - have any items been added or removed? The answer is no, so no change notification happens.
If you want to make your code work with the smallest change possible, you need to swap in a new item rather than just update one of the properties on an existing item.
self.updateValue = function() {
var newVal = { id: 5, thing: false };
self.observableThings()[0] = newVal;
self.observableThings.valueHasMutated();
}
fiddle here
The final thing to say is that kind of the case I outlined above (i.e. updating many members of an array yet only triggering a sigle UI update) is really the only reason that you should be 'forcing' change notifications to happen. If you're having to do something like this then you've gone wrong and saying 'but I'll have to re-write loads of code' is never a good reason not to do it properly.