I have a vm with two objects in it:
vm.obj = {
intObj1: {
title: 'title1'
},
intObj2: {
name: 'name1'
}
}
The vm.obj is bound to the view (I am using the controller as syntax)
I want to have the original data so I cloned the model using lodash:
var originalModelState = _.cloneDeep(vm.obj);
I am watching for changes in the model compared to the original state:
$scope.$watch('vm.obj', function(newValue, oldValue){
if (newValue !== originalModelState){
}
}, true);
Sadly newValue !== originalModelState is always different, which is expected as the references are different. I tried also comparing newValue with oldValue but the issue there is that if the user changes a property for example: vm.obj.intObj1.title = 'new title' and then change back to the original value `vm.obj.intObj1.title = 'title1' I cannot detect that the vm is the same as the original value. How can I do this ?
Have you considered
var originalModelState = JSON.stringify(vm.obj);
...
if (JSON.stringify(newValue) != originalModelState){
}
Comparing objects as strings is imho a very effective and easy way to spot differences, especially when you not know what to look for.
Not sure, if I understood correctly what you wanted to ask. The watcher callback is only executed, if something in your object has changed. The 'true' performs a deep watch.
If you only want to know, if the value deep in your object is equal to the originial value on the same position, you could check in the watcher:
$scope.$watch('vm.obj', function(newValue){
if (originalModelState.intObj1.title === newValue.intObj1.title) {
// do something
}
}, true);
Related
I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/
Trying to get a more in-depth understanding of how Angular treats data binding and understanding it better and one thing is difficult to get my head around -
In Knockout I use a computed to keep track of changes to a property. In Angular it to move this logic into the view, which is a it trivial to me, but if that is the way to do it I understand.
My question is when I am initializing a new entity with Breeze/Angular how do I create computed-like properties that are notified when changes occur to the entities property?
myEntity.fullName = ko.computed(function () {
return myEntity.firstName + ' ' + myEntity.LastName;
});
in Angular would the equivalent be
myEntity.fullName = function () {
return myEntity.firstName + ' ' + myEntity.LastName;
};
And does that properly track the entity?
You are correct to simply make it a function. If your entity as shown is added to the $scope, then you would access the property like so:
<span class="fullname">{{ user.fullName() }}</span>
Whenever Angular runs a $digest cycle, it will check for a change to the bound property. In this instance, it means it will call the fullName() function and check to see if the result has changed. If it has, anything that has a $watch attached to that item — including simple binding — will be notified of the change.
One caveat of this technique, however, is to make sure that the operations being performed within your function are relatively fast, and also have no side effects. Bound functions like this will be called many times throughout the application.
If you need to have a more complex function, it would be better to handle that within the controller, and update a property on the object manually when it changes.
I found the answer on the following website. If you don't do something similar, what you will find is that all functions are ran during the digest phase and are not triggered by the change of a dependent observable or property. The solution below allows you to only trigger the function when a value it uses is changed.
http://www.jomendez.com/2015/02/06/knockoutjs-computed-equivalent-angularjs/
Explains how to duplicate the subscribe and computed feature in knockoutjs
var myViewModel = {
personName: ko.observable('Bob')
};
myViewModel.personName.subscribe(function(oldValue) {
alert("The person's previous name is " + oldValue);
}, null, "beforeChange");
This is what I found as result of my research (this is the AngularJs equivalent) Using the $scope.$watch method see the AngularJs life cycle https://docs.angularjs.org/guide/scope
$scope.myViewModel = {
personName: 'Bob'
};
$scope.$watch(‘myViewModel.personName’, function(newValue, oldValue){
//we are able to have both the old and the new value
alert("The person's previous name is " + oldValue);
});
//knockout computed
var isVendorLoading = ko.observable(),
isCustomerLoading = ko.observable(),
isProgramLoading = ko.observable(),
isWaiting = ko.observable();
var isDataLoading = ko.computed(function () {
return isVendorLoading() || isCustomerLoading() || isProgramLoading() || isPositionLoading() || isWaiting();
});
This is the AngularJs Equivalent for KnockoutJs computed:
$scope.isDataLoading = false;
$scope.$watch(
function (scope) {
//those are the variables to watch
return { isVendorLoading: scope.isVendorLoading, isCustomerLoading: scope.isCustomerLoading, isProgramLoading: scope.isProgramLoading, isWaiting: scope.isWaiting };
},
function (obj, oldObj) {
$timeout(function () {
//$timeout used to safely include the asignation inside the angular digest processs
$scope.isDataLoading = obj.isVendorLoading || obj.isCustomerLoading || obj.isProgramLoading || obj.isPositionLoading || obj.isWaiting;
});
},
true
);
say I have a album list and user can add album
controller.albumList = function($scope, albumService) {
$scope.albums = albumService.query();
$scope.$watch('$scope.albums',function(){
$scope.albums.$save($scope.albums)
})
$scope.addalbum= function(){
$scope.albums.objects.push(album);
}
};
get a album list from server and show them
user can add album
watch the albums list ,when change happend save them to the server.
the problem is the $watch always fired, I did not even trigger the addalbum method, and every time I refresh the page a new album is created.
I follow the the code in todeMVC angular
here is the example code
var todos = $scope.todos = todoStorage.get();
$scope.newTodo = '';
$scope.editedTodo = null;
$scope.$watch('todos', function () {
$scope.remainingCount = filterFilter(todos, { completed: false }).length;
$scope.completedCount = todos.length - $scope.remainingCount;
$scope.allChecked = !$scope.remainingCount;
todoStorage.put(todos);
}, true);
please help me understand this
this is a solution:
$scope.$watch('albums', function(newValue, oldValue) {
if (angular.equals(newValue, oldValue)) {
return;
}
$scope.albums.$save($scope.albums);
}
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
More about a $watch listener: $watch at angularjs docs
Firstly, you do not have to specify the scope object when referencing to a property of the scope. So, replace:
$scope.$watch('$scope.albums', ...)
with the following:
$scope$watch('albums', ...)
Now about your issue. $watch is triggered each time the value of the object / property being watched changes. This includes even those cases when the values are yet to be assigned, such as undefined and null. Thus, if you wish that the save should happen only when a new album is added, you can have code similar to:
//Makes assumption that albums has a length property
$scope.$watch('albums.length', function () {
//Check for invalid cases
if ($scope.albums === undefined || $scope.albums === null) {
return;
}
//Genuine cases.
//Proceed to save the album.
});
With this, the $watch is still triggered in unwanted scenarios but with the check, you avoid saving when the album has not changed. Also, note that your $watch triggers only when the length of the albums object changes. So, if an album itself is updated (say an existing album name is changed), then this watch is not triggered. You can change the watch property based on your needs. The watch property mentioned here works only when a new album is added.
I can successfully do this:
App.SomeCollection = Backbone.Collection.extend({
comparator: function( collection ){
return( collection.get( 'lastName' ) );
}
});
Which is nice if I want to have a collection that is only sorted by 'lastName'. But I need to have this sorting done dynamically. Sometimes, I'll need to sort by, say, 'firstName' instead.
My utter failures include:
I tried passing an extra variable specifying the variable to sort() on. That did not work. I also tried sortBy(), which did not work either. I tried passing my own function to sort(), but this did not work either. Passing a user-defined function to sortBy() only to have the result not have an each method, defeating the point of having a newly sorted backbone collection.
Can someone provide a practical example of sorting by a variable that is not hard coded into the comparator function? Or any hack you have that works? If not, a working sortBy() call?
Interesting question. I would try a variant on the strategy pattern here. You could create a hash of sorting functions, then set comparator based on the selected member of the hash:
App.SomeCollection = Backbone.Collection.extend({
comparator: strategies[selectedStrategy],
strategies: {
firstName: function () { /* first name sorting implementation here */ },
lastName: function () { /* last name sorting implementation here */ },
},
selectedStrategy: "firstName"
});
Then you could change your sorting strategy on the fly by updating the value of the selectedStrategy property.
EDIT: I realized after I went to bed :) that this wouldn't quite work as I wrote it above, because we're passing an object literal to Collection.extend. The comparator property will be evaluated once, when the object is created, so it won't change on the fly unless forced to do so. There is probably a cleaner way to do this, but this demonstrates switching the comparator functions on the fly:
var SomeCollection = Backbone.Collection.extend({
comparator: function (property) {
return selectedStrategy.apply(myModel.get(property));
},
strategies: {
firstName: function (person) { return person.get("firstName"); },
lastName: function (person) { return person.get("lastName"); },
},
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
},
initialize: function () {
this.changeSort("lastName");
console.log(this.comparator);
this.changeSort("firstName");
console.log(this.comparator);
}
});
var myCollection = new SomeCollection;
Here's a jsFiddle that demonstrates this.
The root of all of your problems, I think, is that properties on JavaScript object literals are evaluated immediately when the object is created, so you have to overwrite the property if you want to change it. If you try to write some kind of switching into the property itself it'll get set to an initial value and stay there.
Here's a good blog post that discusses this in a slightly different context.
Change to comparator function by assigning a new function to it and call sort.
// Following example above do in the view:
// Assign new comparator
this.collection.comparator = function( model ) {
return model.get( 'lastname' );
}
// Resort collection
this.collection.sort();
// Sort differently
this.collection.comparator = function( model ) {
return model.get( 'age' );
}
this.collection.sort();
So, this was my solution that actually worked.
App.Collection = Backbone.Collection.extend({
model:App.Model,
initialize: function(){
this.sortVar = 'firstName';
},
comparator: function( collection ){
var that = this;
return( collection.get( that.sortVar ) );
}
});
Then in the view, I have to M-I-C-K-E-Y M-O-U-S-E it like this:
this.collections.sortVar = 'lastVar'
this.collections.sort( this.comparator ).each( function(){
// All the stuff I want to do with the sorted collection...
});
Since Josh Earl was the only one to even attempt a solution and he did lead me in the right direction, I accept his answer. Thanks Josh :)
This is an old question but I recently had a similar need (sort a collection based on criteria to be supplied by a user click event) and thought I'd share my solution for others tackling this issue. Requires no hardcoded model.get('attribute').
I basically used Dave Newton's approach to extending native JavaScript arrays, and tailored it to Backbone:
MyCollection = Backbone.Collection.extend({
// Custom sorting function.
sortCollection : function(criteria) {
// Set your comparator function, pass the criteria.
this.comparator = this.criteriaComparator(criteria);
this.sort();
},
criteriaComparator : function(criteria, overloadParam) {
return function(a, b) {
var aSortVal = a.get(criteria);
var bSortVal = b.get(criteria);
// Whatever your sorting criteria.
if (aSortVal < bSortVal) {
return -1;
}
if (aSortVal > bSortVal) {
return 1;
}
else {
return 0;
}
};
}
});
Note the "overloadParam". Per the documentation, Backbone uses Underscore's "sortBy" if your comparator function has a single param, and a native JS-style sort if it has two params. We need the latter, hence the "overloadParam".
Looking at the source code, it seems there's a simple way to do it, setting comparator to string instead of function. This works, given Backbone.Collection mycollection:
mycollection.comparator = key;
mycollection.sort();
This is what I ended up doing for the app I'm currently working on. In my collection I have:
comparator: function(model) {
var methodName = applicationStateModel.get("comparatorMethod"),
method = this[methodName];
if (typeof(method === "function")) {
return method.call(null, model);
}
}
Now I can add few different methods to my collection: fooSort(), barSort(), and bazSort().
I want fooSort to be the default so I set that in my state model like so:
var ApplicationState = Backbone.Model.extend({
defaults: {
comparatorMethod: "fooSort"
}
});
Now all I have to do is write a function in my view that updates the value of "comparatorMethod" depending upon what the user clicks. I set the collection to listen to those changes and do sort(), and I set the view to listen for sort events and do render().
BAZINGA!!!!
I have a "Cancel" button on my page which should reverts all the changes I made back to the state it was loaded from server..
I guess I need to store an initial state of Backbonejs model and restore a current (changed) state back to initial.
What is the best way to achieve that?
Thank you
FWIW - i wrote a plugin to handle this automatically, specifically with the idea of "cancel" buttons in mind: http://github.com/derickbailey/backbone.memento
model.previousAttributes() returns all of the previous attributes, while model.changedAttributes() returns all the changed attributes, but with their new values (or false if nothing has changed). So you could combine them to write a cancelChanges method in your prototype :
var MyModel = Backbone.Model.extend({
cancelChanges: function() {
var changed = this.changedAttributes();
if(!changed)
return;
var keys = _.keys(changed);
var prev = _.pick(this.previousAttributes(), keys);
this.set(prev, {silent: true}); // "silent" is optional; prevents change event
},
});
I dont believe there's a single method call for returning a model to its unedited state.. but the unedited values are available individually through model.previous(attribute) and collectively via model.previousAttributes.
Here is what I came up with:
var RollbackEnabledModel = Backbone.Model.extend({
initialize: function() {
this._initAttributes = _.clone(this.attributes);
},
parse: function(data) {
this._initAttributes = _.clone(data);
return data;
},
rollback: function() {
this.set(this._initAttributes);
}
});
Take a look at NYTimes' backbone.trackit. It tracks multiple changes to the model instead of only the most recent change like model.changedAttributes() and model.previousAttributes(). From the README:
var model = new Backbone.Model({id:1, artist:'Samuel Beckett', 'work':'Molloy'});
model.startTracking();
model.set('work', 'Malone Dies');
console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies"}
model.set('period', 'Modernism');
console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies", period: "Modernism"}
model.save({}, {
success: function() {
console.log(model.unsavedAttributes()); // >> false
}
});
In addition, the library adds functionality to resetAttributes to
their original state since the last save, triggers an event when the
state of unsavedChanges is updated, and has options to opt into
prompting to confirm before routing to a new context.