Sure this is a very easy question to answer but is there an easy way to determine if any property of a knockout view model has changed?
Use extenders:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.setOriginalValue = function(startingValue) {
target.originalValue = startingValue;
};
target.subscribe(function (newValue) {
// use != not !== so numbers will equate naturally
target.isDirty(newValue != target.originalValue);
});
}
return target;
};
Then:
self.MyProperty= ko.observable("Property Value").extend({ trackChange: true });
Now you can inspect like this:
self.MyProperty.isDirty()
You can also write some generic viewModel traversing to see if anything's changed:
self.isDirty = ko.computed(function () {
for (key in self) {
if (self.hasOwnProperty(key) && ko.isObservable(self[key]) && typeof self[key].isDirty === 'function' && self[key].isDirty()) {
return true;
}
}
});
... and then just check at the viewModel level
self.isDirty()
You can subscribe to the properties that you want to monitor:
myViewModel.personName.subscribe(function(newValue) {
alert("The person's new name is " + newValue);
});
This will alert when personName changes.
Ok, so you want to know when anything changes in your model...
var viewModel = … // define your viewModel
var changeLog = new Array();
function catchChanges(property, value){
changeLog.push({property: property, value: value});
viewModel.isDirty = true;
}
function initialiseViewModel()
{
// loop through all the properties in the model
for (var property in viewModel) {
if (viewModel.hasOwnProperty(property)) {
// if they're observable
if(viewModel[property].subscribe){
// subscribe to changes
viewModel[property].subscribe(function(value) {
catchChanges(property, value);
});
}
}
}
viewModel.isDirty = false;
}
function resetViewModel() {
changeLog = new Array();
viewModel.isDirty = false;
}
(haven't tested it - but you should get the idea)
Consider using Knockout-Validation plug-in
It implements the following:
yourProperty.isModified() - Checks if the user modified the value.
yourProperty.originalValue - So you can check if the value really changed.
Along with other validation stuff which comes in handy!
Cheers
You might use the plugin below for this:
https://github.com/ZiadJ/knockoutjs-reactor
The code for example will allow you to keep track of all changes within any viewModel:
ko.watch(someViewModel, { depth: -1 }, function(parents, child) {
alert('New value is: ' + child());
});
PS: As of now this will not work with subscribables nested within an array but a new version that supports it is on the way.
Update: The sample code was upgraded to work with v1.2b which adds support for array items and subscribable-in-subscribable properties.
I had the same problem, i needed to observe any change on the viewModel, in order to send the data back to the server,
If anyone still intersted, i did some research and this is the best solution iv'e managed to assemble:
function GlobalObserver(viewModel, callback) {
var self = this;
viewModel.allChangesObserver = ko.computed(function() {
self.viewModelRaw = ko.mapping.toJS(viewModel);
});
viewModel.allChangesObserver.subscribe(function() {
callback(self.viewModelRaw);
});
self.dispose = function() {
if (viewModel.allChangesObserver)
viewModel.allChangesObserver.dispose();
delete viewModel.allChangesObserver;
};
};
in order to use this 'global observer':
function updateEntireViewModel() {
var rawViewModel = Ajax_GetItemEntity(); //fetch the json object..
//enter validation code here, to ensure entity is correct.
if (koGlobalObserver)
koGlobalObserver.dispose(); //If already observing the older ViewModel, stop doing that!
var viewModel = ko.mapping.fromJS(rawViewModel);
koGlobalObserver = new GlobalObserver(viewModel, Ajax_Submit);
ko.applyBindings(viewModel [ ,optional dom element]);
}
Note that the callback given (in this case 'Ajax_Submit') will be fired on ANY change that occurs on the view model, so i think it's really recommended to make some sort of delay mechanism to send the entity only when the user finished to edit the properties:
var _entitiesUpdateTimers = {};
function Ajax_Submit(entity) {
var key = entity.ID; //or whatever uniquely related to the current view model..
if (typeof _entitiesUpdateTimers[key] !== 'undefined')
clearTimeout(_entitiesUpdateTimers[key]);
_entitiesUpdateTimers[key] =
setTimeout(function() { SendEntityFunction(entity); }, 500);
}
I'm new to JavaScript and the knockout framework, (only yestarday i started to work with this wonderfull framework), so don't get mad at me if i did something wrong.. (-:
Hope this helps!
I've adapted #Brett Green code and extended it so that we can have AcceptChanges, marking the model as not dirty plus having a nicer way of marking models as trackables. Here is the code:
var viewModel = {
name: ko.observable()
};
ko.track(viewModel);
http://jsfiddle.net/david_freire/3HZEu/2/
I did this by taking a snapshot of the view model when the page loads, and then later comparing that snapshot to the current view model. I didn't care what properties changed, only if any changed.
Take a snapshot:
var originalViewModel = JSON.stringify(ko.toJS(viewModel));
Compare later:
if(originalViewModel != JSON.stringify(ko.toJS(viewModel))){
// Something has changed, but we don't know what
}
Consider a view model as follows
function myViewModel(){
var that = this;
that.Name = ko.observable();
that.OldState = ko.observable();
that.NewState = ko.observable();
that.dirtyCalcultions - ko.computed(function(){
// Code to execute when state of an observable changes.
});
}
After you Bind your Data you can store the state using ko.toJS(myViewModel) function.
myViewModel.Name("test");
myViewModel.OldState(ko.toJS(myViewModel));
You can declare a variable inside your view model as a computed observable like
that.dirtyCalculations = ko.computed(function () {});
This computed function will be entered when there is change to any of the other observables inside the view model.
Then you can compare the two view model states as:
that.dirtyCalculations = ko.computed(function () {
that.NewState(that);
//Compare old state to new state
if(that.OldState().Name == that.NewState().Name()){
// View model states are same.
}
else{
// View model states are different.
}
});
**Note: This computed observable function is also executed the first time when the view model is initialized. **
Hope this helps !
Cheers!!
I like Brett Green's solution. As someone pointed out, the isDirty comparison doesn't work with Date objects. I solved it by extending the subscribe method like this:
observable.subscribe(function (newValue) {
observable.isDirty(newValue != observable.originalValue);
if (newValue instanceof Date) {
observable.isDirty(newValue.getTime() != observable.originalValue.getTime());
}
});
Related
Well met!
I am playing around with Knockoutjs with the goal of having a single ViewModel, which controls multiple sub-viewmodels. This in order to have more control over the views itself and to prevent putting various parts of my view into their own little place. The code below should explain my idea:
ApplicationViewModel
ApplicationViewModel = function () {
var self = this;
// Context (for laziness' sake, no separate VM)
self.activeProject = ko.observable();
// States
self.projectsLoaded = ko.observable(false);
// State-change events
// Let application know that loading of projects has been called
self.projectsLoaded.subscribe(function (newValue) {
if (newValue === true) {
console.log('Projects have loaded');
} else {
console.log('Projects have not loaded');
}
});
// Let application know that selection of a project has happened
self.activeProject.subscribe(function (newValue) {
if (newValue != null) {
// Notify other viewmodels that a project has been (successfully loaded)
// Use hook-pattern to hook into this event
} else {
// Notify something went wrong- present user with a notification
// Application stops processes that are project-dependant
}
});
self.ProjectViewModel = new ProjectViewModel();
};
ProjectViewModel
ProjectViewModel = function () {
var self = this;
self.projects = ko.observableArray();
self.loadProjects = function () {
// Business logic to retrieve projects, think AJAX
var placeHolderProjects = [];
// Find projects somewhere and load them up!
// If something went wrong, notify parent
if (placeHolderProjects.length > 0) {
self.projects(placeHolderProjects);
$root.projectsLoaded(true);
} else {
$root.projectsLoaded(false);
}
};
self.selectProject = function (projectId) {
if (!projectId) {
$.parent.activeProject = null;
return;
}
// Fetch data for project, stuff like membershipId
var loadProjectResult = magicalLoadFunction(projectId);
if (loadProjectsResult === true) {
$root.activeProject(projectId);
} else {
$root.activeProject(projectId);
}
// Exit
return;
}
/********** Constructor logic
****************************/
self.loadProjects();
};
So basically, what I am looking for, is a way to:
- Control parent/child properties from their respective child/parent inside the viewmodels.
I am looking into AngularJS as well, but I'd really like to get this working in KnockoutJS first :) Immediate problem, is that I can't get $root/$parent to work. I bind the ApplicationViewModel in a $(document).ready() handler, unsure if I have to actually bind the sub-viewmodels to the view as well. I have bound ApplicationViewModel to the body element.
Thanks for reading and, possibly for answering/helping me get on my way :)
The answer provided by #jansommer proved successful.
I changed the following line (added this as a parameter):
self.ProjectViewModel = new ProjectViewModel(this);
And that was what was needed.
Thanks!
I have a firebaseObject (MyFirebaseService.getCurrentUser()) bind to $scope.user.
After binding successful, I loop tho the object to see if the object contain "associatedCourseId" equal to some value ($stateParams.id). If does, the $scope.finishLessonCount count up. The problem is, when I add new Object inside the firebaseObject (that bindto user) via other page OR inside firebase, the finishLessonCount value won't change as what I expect for 3 way binding. I need to refresh the page to see the finishLessonCount reflect the true value. What is wrong? I want the finishLessonCount change using the compare function as I add more finishedLessons into the firebaseObject. Please see code below:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user").then(function(){
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
UPDATE 1 according to #Kato solution:
I decide to use Extending firebaseOject way to solute this problem. But still, it does not. I did not use factory here to simplify thing since I need to pass in courseId to do the operation. Here is my code:
function countLessons(lessons, courseId) {
var count = 0;
for(var key in lessons) {
if( lessons[key].associatedCourseId == courseId) {
count++;
}
}
return count;
}
var UserWithLessonsCounter = $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons, $stateParams.id);
}
});
var refTemp = new Firebase($rootScope.baseUrl + "users/" + $rootScope.userId);
var userTemp = new UserWithLessonsCounter(refTemp);
userTemp.$bindTo($scope, "userTemp").then(function(){
console.log($scope.userTemp);
});
userTemp.$watch(function() {
console.log("Does this run at all? " + $scope.userTemp.lessonCount);
});
I update the user object, the lessonCount value did not change unless I refresh the page. And the console.log inside $watch did not run at all. What is wrong?
The promise returned by $bindTo is called exactly once. It's not an event listener. You can't listen to this to get updated each time there is a change.
Please read the guide, start to finish, and read about Angular's $watch method before continuing down this route, as with some fundamental knowledge, this should not have been your first instinct.
A beginner approach would be to utilize $watch:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user");
$scope.$watch('user', function() {
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
Or, having familiarized with the AngularFire API, one might pick $scope.user.$watch() in place of the scope method, which would prove more efficient.
Having written a large portion of the AngularFire code, I would pick the $extend tool, which was added precisely for use cases like this:
// making some assumptions here since you haven't included
// the code for your firebase service, which does not seem SOLID
app.factory('UserWithLessonsCounter', function($firebaseObject) {
return $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons);
return changed;
}
});
});
function countLessons(lessons) {
var count = 0;
for(var key in lessons) {
if( lessons.hasOwnProperty(key) ) {
count++;
}
}
return count;
}
And now in your controller:
app.controller('...', function($scope, UserWithLessonsCounter) {
var ref = new Firebase(...);
var user = new UserWithLessonCounter(ref);
user.$bindTo($scope, 'user');
user.$watch(function() {
console.log($scope.user.lessonCount);
});
});
So I've used Backbone and Angular quite a bit and gotten used to data binding / view updating within those ecosystems, but I don't know how I would achieve this in plain JS (no frameworks/libraries).
Right now I have a simple UserList, and I would like to watch for changes to it and trigger and update of an unordered list when it happens.
var ContactList = {
list: [],
push: function(obj) {
this.storage.push(obj);
},
remove: functon(obj) {
return this.storage.splice(this.storage.indexOf(obj), 1);
}
};
var Contact = function(attributes) {
this.attributes = attributes || {};
};
Contact.prototype.get = function(property) {
return this.attributes[property];
};
Contact.prototype.set = function(property, value) {
this.attributes[property] = value;
};
Ideally the following would automatically add to a list. I could just add a callback to the push and remove methods, but that seems like it doesn't scale very well if I get to a point where I'm adding more methods to operate on my list. I've been reading a bit about the observer pattern, but not sure if that's really what I'm looking for here.
You don't want to pass the callback to every call of ContactList.push and ContactList.remove and all the ContactList methods you are yet to write. Instead, the ContactList will know when he has changed and then announce that fact to the world. In a simple implementation, ContactList could have his own onChange method which he could call:
var ContactList = {
list: [],
push: function (obj) {
this.list.push(obj);
this.onChange();
},
remove: function (obj) {
var index = this.list.indexOf(obj);
if (index > -1) {
var removed = this.list.splice(index, 1);
this.onChange();
return removed;
} else {
return null;
}
}
};
You would then, obviously, have to define ConactList.onChange:
ContactList.onChange = function () {
console.log(this.list);
/* update your ul element */
};
This solution will not allow you to add subscribers dynamically to Contact List's change event, but it might be a helpful starting point.
I am designing a Javascript-based Ohms law calculator (voltage, resistance, current) using knockout.js.
I want the ability of the user being able to select what is calculated, , e.g. voltage, resistance, or current, given the other two parameters, via radio buttons.
So my question is, can you change a ko.observable into a ko.computed and vise versa, after ko.applyBindings() has been called?
My initial attempts say no, I have tried this and slaved over the non-working code for hours, trying to get it to work.
You can't do it that way, but you can make all of them read/write ko.computeds that store a "shadow" value when written to and return that value when read from if they aren't the selected quantity (and return a calculated value if they aren't)
You dont even need a writable computed for this like ebohlman suggests
A simple demo
http://jsfiddle.net/K8t7b/
ViewModel = function() {
this.selected = ko.observable("1");
this.valueOne = ko.observable(1);
this.valueTwo = ko.observable(5);
this.result = ko.computed(this.getResult, this);
}
ViewModel.prototype = {
getResult: function() {
if(this.selected() === "1") {
return this.valueOne() - this.valueTwo();
}
return this.valueOne() + this.valueTwo();
}
};
ko.applyBindings(new ViewModel());
edit: hm, if you want the result to be presented in the correct value textbox you need to make them writable computed like ebohlman suggets
As ebohlman mentioned, the vital thing I was missing was shadow-variables, and the use of separate read/write procedures (a recently added feature to knockout) for ko.computed.
The code for one of the three variables is:
this.voltageS = ko.observable();
this.voltage = ko.computed({
read: function () {
if(this.calcWhat() == 'voltage')
{
console.log('Calculating voltage');
if(this.currentS == null)
return;
if(this.resistanceS == null)
return;
this.voltageS(this.currentS()*this.resistanceS());
return(this.currentS()*this.resistanceS());
}
else
{
console.log('Reading from voltage');
return this.voltageS();
}
},
write: function (value) {
console.log('Writing to voltage');
this.voltageS(value)
},
owner: this
});
I have created a JSFiddle here, which demonstrates being able to switch between which variable is calculated.
Another key part to this code is that on read, if it did happen to be the selected variable, as well as calculating it from the other two, I also had to write this result back to the shadow variable. This preventing some of the variables from mysteriously dissapearing/reappearing when the selected variable was changed.
I'm trying to implement some kind of undo feature in my project using KnockoutJS. For this purpose, I'm using this extender:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.subscribe(function (newValue) {
// Push to "states" array a JS representation of the viewModel
// so I can get a stack of changes
states.push(ko.toJS(track.myViewModel));
target.isDirty(newValue != target.originalValue);
target.originalValue = newValue;
});
}
return target;
};
Then I apply the extender to an object into the viewModel:
this.myViewModel = {
label: ko.observable("Label").extend({ trackChange: this });
}
And when I want to undo an action, I do this:
ko.applyBindings(ko.mapping.fromJS(states[statesPointer]));
This is ok in order to get the old values, but the extend function in the observable is lost so new changes are not saved in the "states" stack.
Suggestions?
Thanks in advance,
Elian.
I think you should not create new model instance performing undo op, but just update observable properties of existing model:
// you do:
// ko.applyBindings(ko.mapping.fromJS(states[statesPointer]));
// I would do:
ko.mapping.fromJS(states[statesPointer], myViewModel);
Looks like your myViewModel has own context, so you have to modify my code to get correct ref to the model.