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
);
Related
I have code that uses AngularJS v1.5.0 and creates multiple forms with an ng-repeat like this. Note that inside the form I show the form details between xx and xx:
<div ng-click="wos.wordFormRowClicked(wf)"
ng-form="wos.wordFormNgForm_{{wf.wordFormId}}"
ng-repeat="wf in wos.word.wordForms">
xx {{ wos['wordFormNgForm_1465657579'] }} xx
When the runs I am able to see the form details appear between the xx and xx and I am able to query the state of the form like this:
wordFormCheckAndUpdate = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
wordEditSubmit = (): ng.IPromise<any> => {
var self = this;
return this.wordFormCheckAndUpdate().then(
() => {
return self.$http({
url: self.ac.dataServer + "/api/word/Put",
method: "PUT",
data: self.word
})
.then(
(response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine();
self.uts.remove(self.words, 'wordId', self.word.wordId);
response.data.current = true;
self.words.push(response.data);
var a = 99;
},
(error: ng.IHttpPromiseCallbackArg<any>): any => {
self.ers.error(error);
return self.$q.reject(error);
});
}
);
}
My problem is that if I then try to repeat this:
setDirty = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
}
then the controller object self[wordFormNgForm] is no longer defined.
For reference. Here is how new wordForms are created:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty();
}
Here is the remove function:
remove = (arr, property, num) => {
arr.forEach((elem, index) => {
if (elem[property] === num)
arr.splice(index, 1);
})
};
Does anyone have any advice as to how I could solve this problem
Your problem could have been explained well with a demo reproducing the issue. Nevertheless, I've partially reproduced your problem in a fiddle here (not with Typescript though, it's just vanilla JS).
What I suspect happens when you first add a wordForm object with the wordFormAdd() method, is that, if you try to reference the FormController object associated with the ng-form in the same method immediately, then it might be too early to do so, because the $digest loop might not have been completed.
This is because as soon as you click and trigger the wordFormAdd() function, a new emptyWordForm object is added to the word.wordForms array and immediately ng-repeated in your view. But, the controller hasn't had enough time to associate the newly created ng-form object with itself, so you may end up with referencing an undefined object.
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty(); //<== too early to do so
}
To overcome this, you should wrap that portion of the code within a $timeout wrapper. This ensures that Angular's so-called dirty check (or simply the digest loop) is finished.
Also note that keeping a single wordNgForm or wordFormId reference doesn't make sense, because you might dynamically add other forms, each of which may be associated with a new wordNgForm key and wordFormId.
I would suggest doing the above like so:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
...
this.word.wordForms.push(emptyWordForm);
this._timeout(function(){ // $timeout injected and assigned to this._timeout in controller definition
var wordFormNgForm = 'wordFormNgForm_' + this.wordFormId;
this[wordFormNgForm].$setDirty(); // <==
});
}
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
A possible reason where the watched value in your view ({{ wos['wordFormNgForm_1465657579'] }}) becomes undefined, is that you are fetching new values and storing a copy of them in the controller's self.word property:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data); // <==
...
},
By doing so, the collection under word.wordForms that was previously ng-repeated in the view is changed and the watched value is no longer a valid reference to an item of this collection.
Meanwhile, self['wordNgForm'] in the wordEditSubmit certainly isn't associated with a FormController object as far as the ng-repeat in your view is concerned. This is because the FormController object keys associated with an ng-form must have a format (as imposed by you) similar to something like wordFormNgForm_1465657579. Therefore, here too, you are referencing an undefined property under self['wordNgForm']:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine(); // <==
...
},
This looks like standard issue with java script that this means different things depending what called the function.
What I would advise is to generate form names and place them in wos.word.wordForms collection and bind them from there. Doing gymnastics like ng-form="wos.wordFormNgForm_{{wf.wordFormId}}" and var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId; feel quite awkward.
If there is a reason you are not using this approach please tell me, there might be a different solution :)
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);
Simple question here.
I have this watch:
// Watch our model
$scope.$watch(function () {
// Watch our team name
return self.model.team.data.name;
}, function (name) {
console.log(name);
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
The team model is pull in from the database as a promise (hence the data) so when the watch first fires self.model.team has not been set so it is null.
How can I get my watch to either wait until it has been set or add a check into the return function of the watch?
Use a watch expression instead of a function. This will catch any errors with missing objects and return undefined.
// Watch our model
$scope.$watch('self.model.team.data.name', function (name) {
console.log(name);
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
There is no magic here - if one of the variables you are accessing could be null/undefined, then you cannot get its property if it's null/undefined. So, you have to guard against that:
$scope.$watch(
function(){
return (self.model.team && self.model.team.data.name) || undefined;
},
function(v){
// ...
});
The only "magic" is when you "$watch" for expressions, but the expressions need to be exposed on the scope. So, you could do:
$scope.model = self.model;
$scope.$watch("model.team.data.name", function(v){
// ...
});
But, really, you have to ask yourself why you need a $watch here to begin with. It seems to me that you are getting the team asynchronously once - it does not look like it will change except by maybe another async call. So, just handle that when you receive the data without the $watch:
someSvc.getTeam() // I made an assumption about a service that pulls the data from db
.then(function(team){
var name = team.data.name;
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
An unnecessary $watch is expensive - it is evaluated on every digest cycle, so, it's best to reduce the number of $watchers.
I'm trying to implement a grid in angular, using server-side sorting, server-side pagination and server-side filtering. Using ui-grid (unstable), I added ui.grid.paging and got everything up and running. Very nice, kudos to the developers.
However.... $scope.gridApi.core.on.filterChanged is firing for every key pressed, so when I search for "Patrick" in the givenname column, seven events are fired and seven get-requests hit my server. Even worse, since it is a large set I'm filtering, this operation is pretty expensive and the results my even overtake each other, like the most specific filter getting the fastest result, triggering success before a less specific result is processed.
I'd like to slow it down, like "fire after entry has stopped". I'm pretty much new to javascript and REST, this is my first real-world-project. I would really appreciate any ideas how to handle this issue. It feels like a common scenario, so there might be some standard solutions or best practices I'm missing.
Yours,
Patrick.
If you wanna go "all angular" I would suggest to use a $timeout inside the on.filterChanged event handler:
if (angular.isDefined($scope.filterTimeout)) {
$timeout.cancel($scope.filterTimeout);
}
$scope.filterTimeout = $timeout(function () {
getPage();
}, 500);
where 500 is the time (in milliseconds) you would like to wait between each on.filterChanged event before going to the server and getPage() is the function that actually goes to the server and retrieves the data.
Don't forget to cancel your $timeout on the controller's 'destroy' event:
$scope.$on("$destroy", function (event) {
if (angular.isDefined($scope.filterTimeout)) {
$timeout.cancel($scope.filterTimeout);
}
});
I was dealing with the same problem and I came up with another solution which IMHO is more "Angular-friendly". I used the ngModelOptions directive introduced in Angular 1.3.
I replaced uiGrid's default filter template ("ui-grid/ui-grid-filter") by a custom one, and configured the ngModelOptions directive on the input with a default debounce value of 300 ms and 0 ms for blur.
This is a sample template based on ui-grid 3.0.5 original template where I also changed default CSS classes by Bootstrap classes:
$templateCache.put('ui-grid/ui-grid-filter-custom',
"<div class=\"ui-grid-filter-container\" ng-repeat=\"colFilter in col.filters\" ng-class=\"{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }\">" +
"<div ng-if=\"colFilter.type !== 'select'\"><input type=\"text\" class=\"input-sm form-control\" ng-model=\"colFilter.term\" ng-model-options=\"{ debounce : { 'default' : 300, 'blur' : 0 }}\" ng-attr-placeholder=\"{{colFilter.placeholder || ''}}\" aria-label=\"{{colFilter.ariaLabel || aria.defaultFilterLabel}}\"><div role=\"button\" class=\"ui-grid-filter-button\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term !== null && colFilter.term !== ''\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\"> </i></div></div>" +
"<div ng-if=\"colFilter.type === 'select'\"><select class=\"form-control input-sm\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || aria.defaultFilterLabel}}\" aria-label=\"{{colFilter.ariaLabel || ''}}\" ng-options=\"option.value as option.label for option in colFilter.selectOptions\"><option value=\"\"></option></select><div role=\"button\" class=\"ui-grid-filter-button-select\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term != null\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\"> </i></div></div>" +
"</div>"
);
The final step for this to work is to set this template in every columnDef where you enable filtering:
columnDefs: [{
name: 'theName',
displayName: 'Whatever',
filterHeaderTemplate: 'ui-grid/ui-grid-filter-custom'
}]
Unfortunately, I couldn't find any way to define this template globally, so I had to repeat the filterHeaderTemplate everywhere... That's the only drawback, but on the other hand, you can also add more filters to your custom template if you need to.
Hope it helps!
I'd recommend that you take a look at reactive-extensions for Javascript. Reactive is built for precisely these kinds of scenarios. There is a method called .Throttle(milliseconds) which you can attach to an observable that will call a handler (to hit your API) after x milliseconds have passed for the user not entering anything.
Rx-Js and angular play well together
Here is an example from one of my projects of doing something similar:
observeOnScope($scope, 'tag', true)
.throttle(1000)
.subscribe(function (data) {
if (data.newValue) {
$http({
url: api.endpoint + 'tag/find',
method: 'GET',
params: {text: data.newValue}
}).then(function (result) {
$scope.candidateTags = result.data;
})
}
});
This code takes $scope.tag and turns it into an observable. The .throttle(1000) means that after 1 second of $scope.tag not changing the subscribe function will be called where (data) is the new value. After that you can hit your API.
I used this to do typeahead lookup of values from my API so hitting the backend everytime a letter changed obviously wasn't the way to go :)
JavaScript setTimeout function can also be used for giving delay as given below.
$scope.gridApi.core.on.filterChanged( $scope, function() {
if (angular.isDefined($scope.filterTimeout)) {
clearTimeout($scope.filterTimeout);
}
$scope.filterTimeout = setTimeout(function(){
getPage();
},1000);
});
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.