Reputation: 1790
I am trying to map data so that elements only get re-rendered when values have actually changed.
{
Apps : [
{
"Categories" : [{
"Name" : "#Some,#More,#Tags,#For,#Measure"
}
],
"Concentrator" : "",
"Health" : 1,
"Id" : 2648,
"Ip" : "1.1.1.1",
"IsDisabled" : true,
"IsObsolete" : false,
"Name" : "",
"Path" : "...",
"SvcUrl" : "http://1.1.1.1",
"TimeStamp" : "\/Date(1463015444163)\/",
"Type" : "...",
"Version" : "1.0.0.0"
}
...
]
...
}
var ViewModel = function() {
self.Apps = ko.observableArray([]);
}
var myModel = new ViewModel();
var map = {
'Apps': {
create: function (options) {
return new AppModel(options.data);
},
key: function(data) { return ko.utils.unwrapObservable(data.Id); }
}
}
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(',');
ko.mapping.fromJS(data, { }, this);
return this;
}
function UpdateViewModel() {
return api.getDashboard().done(function (data) {
ko.mapping.fromJS(data, map, myModel);
});
}
loopMe(UpdateViewModel, 5000);
function loopMe(func, time) {
//Immediate run, once finished we set a timeout and run loopMe again
func().always(function () {
setTimeout(function () { loopMe(func, time); }, time);
});
}
<script type="tmpl" id="App-template">
<div>
<!-- ko foreach: Categories -->
<span class="btn btn-default btn-xs" data-bind="text:$data"></span>
<!-- /ko -->
</div>
</script>
On the first run of UpdateViewModel I will see 5 spans as expected. On the second call, receiving the same data, it gets updated to a single span that says [Object object] which is because it still thinks Categories is an array of objects instead of an array of strings.
Everything seems fixed if I change 'create' to 'update' in my map, however it seems that the spans are then re-rendered every time regardless if data changed or not.
Can anyone lend me a hand in the direction I need to go so that I can
Here is a Fiddle showing the behavior
Upvotes: 2
Views: 321
Reputation: 63830
The problem is with these lines:
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(','); // <-- mainly this one
ko.mapping.fromJS(data, { }, this);
return this;
}
There's two problems:
You mutate the data
object which (at least in our repro) mutates the original object that data
references to. So first time one of the fakeData objects is passed in, that one is mutated in place, and will forever be "fixed".
You mutate it in the AppModel
constructor function, which is only called the first time. According to your key
function, the second time the constructor should not be called, but instead ko-mapping should leave the original object and mutate it in place. But it will do so with a "wrongly" formatted data.Categories
property.
The correct fix seems to me to be in your data layer, which we have mocked in the repro, so it makes little sense for my answer to show you how.
Another more hacky way to do this would be to have an update
method in your mapping like so:
update: function(options) {
if (!!options.data.Categories[0].Name) {
options.data.Categories = options.data.Categories[0].Name.split(',');
}
return options.data;
},
When it encounters an "unmodified" data object it'll do the same mutation. See this jsfiddle for that solution in action.
Upvotes: 1