Based on the answer of this question, I try to get the value before change in an observable with the following code.
var phoneBook;
function debug(s) {
$("#log").append('<br>' + s);
}
function PhoneNumber(data) {
var self = this;
self.phoneType = ko.observable();
self.phoneNumber = ko.observable();
self.phoneNumber.subscribe(function(newValue) {
debug('newvalue: ' + newValue);
});
self.phoneNumber.subscribe(function(previousValue) {
debug(previousValue);
}, self, "beforeChange");
ko.mapping.fromJS(data, PhoneNumber.mapping, self);
}
PhoneNumber.mapping = {};
function Contact(data) {
var self = this;
self.name = ko.observable();
self.email = ko.observable();
self.phones = ko.observableArray();
ko.mapping.fromJS(data, Contact.mapping, self);
}
Contact.mapping = {
phones: {
create: function(options) {
return new PhoneNumber(options.data);
}
}
};
function PhoneBook(data) {
var self = this;
self.contacts = ko.observableArray();
ko.mapping.fromJS(data, PhoneBook.mapping, self);
}
PhoneBook.mapping = {
contacts: {
create: function(options) {
return new Contact(options.data);
}
}
};
var phoneBookData = {
contacts: [{
name: 'John',
email: 'address@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '999-888-777'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777'
}]
},
{
name: 'John2',
email: '222address@domain.com',
phones: [{
phoneType: '22Home Phone',
phoneNumber: '22999-888-777'
}, {
phoneType: '22Business Phone',
phoneNumber: '444-888-777'
}]
}
]
};
var phoneBookDataOther = {
contacts: [{
name: 'peter',
email: 'address@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '999-888-777'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777'
}]
},
{
name: 'almond',
email: '222address@domain.com',
phones: [{
phoneType: '22Home Phone',
phoneNumber: '22999-888-777'
}, {
phoneType: '22Business Phone',
phoneNumber: '444-888-777'
}]
}
]
};
function dofunc() {
ko.mapping.fromJS(phoneBookDataOther, phoneBook);
}
$(document).ready(function() {
phoneBook = new PhoneBook(phoneBookData);
ko.applyBindings(phoneBook);
setTimeout(dofunc, 5000)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<ul data-bind="foreach: contacts">
<li>
<div data-bind="text: name"></div>
<div data-bind="text: email"></div>
<ul data-bind="foreach: phones">
<li>
<span data-bind="text: phoneType"></span>:
<span data-bind="text: phoneNumber"></span>
</li>
</ul>
</li>
</ul>
<div>
<p id="log"></p>
</div>
The change event happens when the mapping plugin is called a again time (here after a timeout of 5 seconds), but the previousValue
always comes out as undefined
.
What i am doing wrong?
Here is the jsfiddle, too: https://jsfiddle.net/icinema/ungbz27s/1/
The problem here is that you are using the mapping plugin wrong, and that your test data makes no sense.
There will only ever be a "previous" value, when you write a new value to the exact same observable. But the mapping plugin will throw away all your viewmodels and make new ones when you map a completely different set of data.
How is it supposed to know that the object with the name "John" in the first round is supposed to be the same person that has the name "peter" in the second round? It can't. So it throws out all the contacts including all their phone numbers and makes new ones. There never is a "previous" value in this scenario.
What you need is
ko.mapping.fromJS
.key
function to the mapping configuration.Read the documentation of the mapping plugin - read the entire thing, it's not that much to begin with.
In the below example I used name
as the key for contacts and phoneType
as the key for phones, and I amended the test data so that they have the same names and phone types across both sets. You probably want to use a contact ID number as the key instead of the name.
The advantage of using the key
function is that knockout will only update the phone number text in the DOM, instead of throwing out and recreating the whole <li>
and everything in it, because it can recognize existing viewmodel instances and keep them. This will cut down on rendering time.
/* global ko, $ */
function debug(s) {
$("#log").append('<br>' + s);
}
function PhoneNumber(data) {
var self = this;
self.phoneType = ko.observable();
self.phoneNumber = ko.observable();
self.phoneNumber.subscribe(function(newValue) {
debug('new value: ' + newValue);
});
self.phoneNumber.subscribe(function(previousValue) {
debug('previous value: ' + previousValue);
}, self, "beforeChange");
ko.mapping.fromJS(data, PhoneNumber.mapping, self);
}
PhoneNumber.mapping = {};
function Contact(data) {
var self = this;
self.name = ko.observable();
self.email = ko.observable();
self.phones = ko.observableArray();
ko.mapping.fromJS(data, Contact.mapping, self);
}
Contact.mapping = {
phones: {
create: function(options) {
return new PhoneNumber(options.data);
},
key: function (data) {
return ko.unwrap(data.phoneType);
}
}
};
function PhoneBook(data) {
var self = this;
self.contacts = ko.observableArray();
ko.mapping.fromJS(data, PhoneBook.mapping, self);
}
PhoneBook.mapping = {
contacts: {
create: function(options) {
return new Contact(options.data);
},
key: function (data) {
return ko.unwrap(data.name);
}
}
};
var phoneBookData = {
contacts: [{
name: 'John',
email: 'john@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '999-888-777-old'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777-old'
}]
},
{
name: 'Peter',
email: 'peter@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '22999-888-777-old'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777-old'
}]
}
]
};
var phoneBookDataOther = {
contacts: [{
name: 'John',
email: 'john@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '999-888-777-new'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777-new'
}]
},
{
name: 'Peter',
email: 'peter@domain.com',
phones: [{
phoneType: 'Home Phone',
phoneNumber: '22999-888-777-new'
}, {
phoneType: 'Business Phone',
phoneNumber: '444-888-777-new'
}]
}
]
};
$(document).ready(function() {
var phoneBook = new PhoneBook(phoneBookData);
ko.applyBindings(phoneBook);
debug('<hr>');
setTimeout(function dofunc() {
ko.mapping.fromJS(phoneBookDataOther, phoneBook);
}, 3000);
});
#log { font-family: monospace; font-size: small; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<ul data-bind="foreach: contacts">
<li>
<div data-bind="text: name"></div>
<div data-bind="text: email"></div>
<ul data-bind="foreach: phones">
<li>
<span data-bind="text: phoneType"></span>:
<span data-bind="text: phoneNumber"></span>
</li>
</ul>
</li>
</ul>
<hr>
<div id="log"></div>