Does an AngularJS watch fire upon object initialization - javascript

I'm currently working on an AngularJS app
I have an object which contains some boolean flags. I created a watch for one of such bool.
Does the watch fire upon object creation? Can the watch fire at random time even the boolean flag hasn't changed?

So we have (I'm working with Gianluca):
scope.$watch("chartData.selectedIndicator", function() {
if (chartData.selectedIndicator !== -1){
highlightMessageIndicator(chartData.selectedIndicator);
}
}, true);
...despite not having updated chartData.selectedIndicator, this watch is still being hit and I am wondering if it is because within our chartData factory we are initialising selectedIndicator and this is why the watch is being hit?
angular
.module("app")
.factory("chartData", [..., chartData]);
function chartData(...) {
var chartData = {
selectedIndicator : -1,
I wonder if this would potentially be a case of just checking for newValue !== oldValue as suggested then?

Related

Why is $doCheck called twice when the view changes, and what can I do about it?

In the following Angular 1.7.5 code, I hook up a text box to a certain controller field with ng-model and use $doCheck to do something whenever the value changes. Note that in my actual project, the value I'm watching is an object and I need to find out whenever any of its fields change, so I have to use $doCheck. I know it's overkill for a textbox.
HTML
<div ng-app="app" ng-controller="TheController as ctrl">
<input ng-model="ctrl.value" />
</div>
JS
angular.module("app", [])
.controller("TheController",
class TheController
{
constructor()
{
this.value = "";
}
$doCheck()
{
console.log("check: " + this.value);
}
}
);
Whenever the value in the text box changes, you can see that $doCheck is called twice (both times with the new value). Why is this? How can I filter out the "pointless" $doCheck calls and just get the ones where the value actually changed?
The $digest loop keeps iterating until the model stabilizes, which means that the $evalAsync queue is empty and the $watch list does not detect any changes.
This means that $doCheck will called a minimum of two times. Once to detect changes and again to detect that there are no new changes. If there are more changes or functions added to the $evalAsync list. It will be called again and again to the limit set by $rootScopeProvider.digestTtl(limit).
If detecting changes, you must store the previous value(s) for comparison to the current values.
this.$doCheck = function() {
var newValue;
var oldValue;
oldValue = newValue;
newValue = itemOfInterest();
if (newValue != oldValue) {
//Code to act on changes
};
};
For more information, see
AngularJS Developer Guide - Integration with the browser event loop
AngularJS $compile Service API Reference - Life-cycle Hooks

ko.toJS() gets 'undefined' for variable initialized to ""

I have a project in ASP.NET MVC 4 that uses knockoutjs to handle client-side stuff like keeping track of if a certain field has changed.
In the class declaration for my ViewModel, i have 2 observables, both initialized to "":
private _observables = {
query: ko.observable(""),
object: ko.observable("")
}
In the close function of a dialog box, I check both observables using the isDirty() method to find out if they've changed, and prompt the user about saving if changes are detected.
I've stepped through and figured out that object doesn't appear to be tracked correctly, despite the line above. Inside isDirty(), knockout pulls the current state like this:
target.isDirty = function () {
var currentState = ko.toJS(target);
....logic
returns dirty/notdirty
After the ko.toJS() call, the object field of currentState is always undefined, which causes it to fail the state check- because the initial state is properly recorded as "".
Even if I use self._observables.object("") to explicitly set object before the call to isDirty(), it's still undefined after the ko.toJS() call inside the 'dirty-checker'.
I thought the issue might be the binding in the view- it's bound to a hidden field that doesn't get user input, however as long as object is initialized, I don't see how that initial value is being lost/overwritten.

Watching an object in AngularJS

I am using AngularJs 2 and i am having difficulties watching an object.
I have an object like the following in a directive:
scope.timelineFilter = {hasCapacity: true, onlyActivated: true, companyIdList: []};
I have 2 JQuery functions who change certain values of the object (Soon implementing the third)
$("[name='hasCapacity']").bootstrapSwitch().on('switchChange.bootstrapSwitch', function(event, state) {
scope.timelineFilter.hasCapacity = state;
});
$("[name='activeOnly']").bootstrapSwitch().on('switchChange.bootstrapSwitch', function(event, state) {
scope.timelineFilter.onlyActivated = state;
});
These functions work correctly. If i log the scope.timelineFilter after changing it, i can see the change from true to false (or the other way around)
Now i want to do a call to the backend each time this object changes. So i tried implementing a watcher like this:
scope.$watchCollection('timelineFilter', function() {
console.log("Changed");
}, true);
I did set the third parameter to true for reference checking.
The problem is, this event only fires when the page is loaded but when changing the properties, it does never fire anymore.
Also tried without the third parameter, using scope.$watch with and without the third parameter. Same result.
I searched SO, most people tried to watch arrays with objects, so that isn't viable for me as the backend expects an object.
How can i solve this problem? Is the problem in JQuery? By changing it in the JQuery method it does not register for the watcher?
Thanks!
You are doing things outside of angular's digest cycle. You have to invoke it yourself:
$("[name='hasCapacity']").bootstrapSwitch().on('switchChange.bootstrapSwitch', function(event, state) {
scope.timelineFilter.hasCapacity = state;
scope.$digest();
});
$("[name='activeOnly']").bootstrapSwitch().on('switchChange.bootstrapSwitch', function(event, state) {
scope.timelineFilter.onlyActivated = state;
scope.$digest();
});

How to ignore Angular $watch event when I myself have caused it?

I have a custom UI element with link to ngModel:
scope:
{
ngModel : "="
}
There are two ways how the attached model might change:
it is changed from outside - in this case I want to update UI of my custom element
it is changed from inside - in this case I want to ignore the changes because my UI is already up-to-date
I have a watch:
$scope.$watch("ngModel", function(newValue){
// here I have complex logic to traverse newValue
// to see if it matches my current UI state
// if it matches, then I return
// if it does not match, then I sync my UI to the newValue
});
and I have a function which pushes current UI state to the model:
function pushSelectionToModel() {
// some code
$scope.ngModel = newState;
// some code
}
Everything works fine, but in cases when user is scrolling through my UI directive fast, ngModel watch is being triggered each time. My code to detect if newValue matches my current UI state is fairly complex, thus causing performance issues.
A natural solution seems to be somehow to ignore the $watch in case when I have just called pushSelectionToModel. I cannot just disable the watch in pushSelectionToModel before update and enable after that because the watch is executed later, after pushSelectionToModel has exited.
How do I tell Angular that for some particular model assignment operation I don't want to trigger some particular $watch?
Here is the relevant simplified Plunkr example
Essentially I want to prevent updateUi() from being called twice when I click Apply button. But the example is simplified, in reality I can't directly assign or compare innerName and ngModel values because in my actual code the values differ because of some transformations.
I've sovled a similar problem, by adding the following statement:
$scope.$watch('ngModel', function(newValue){
if($scope.innerName !== newValue){
// now execute code
http://plnkr.co/edit/r9sQax4VNqBraimQi9pz
but its more of an workaround...

Angular.js: In child controller inside ng-repeat, how to propagate up model changes

In my angular application, I am using an ng-repeat and, inside each, a child controller with a form for each item. I'm also using a debounce so that I can auto-save the data. But the issue is that after saving the data (with Restangular), the changes are not propagated back up to the array in the parent. A simple example will work using angular.copy, but this has issues with Restangular and its replacement, Restangular.copy, does not function exactly the same way. I also tried explicitly setting the item to the right index on the array, but it causes my cursor to lose focus in the input.
Here is a simplified version of my child controller's code. And here is a full JS Bin. Is there another approach to solve this problem?
$scope.personCopy = angular.copy($scope.person);
// Debounce and auto-save changes
$scope.$watch('personCopy', function(newVal, oldVal) {
if (newVal && newVal != oldVal) {
if (timeout) $timeout.cancel(timeout);
timeout = $timeout(savePerson, 1000);
}
}, true);
var savePerson = function() {
// (In my real app, the following is inside a save callback)
// Method 1: (doesn't work at all)
$scope.person = $scope.personCopy
// Method 2: (works with angular.copy, but not Restangular.copy)
// angular.copy($scope.personCopy, $scope.person);
// Method 3: (works, but cursor loses focus)
// $scope.people[$scope.$index] = $scope.personCopy;
};
Method 3 will work for you if you add "track by" to your ng-repeat:
<li ng-repeat='person in people track by $index' ng-controller='EditPersonController'>
Here's that working: http://jsbin.com/jitujaro/3/edit
The reason you're losing focus is the DOM for that ngRepeat is currently being recreated when you update people. So the element that the focus was on is gone. When using track by Angular knows it doesn't need to recreate those DOM elements.
The reason method 1 and 2 don't work is Javascript's prototypal inheritance. Upon writing to a variable that is on a parent scope a new local copy of that variable is made on the local scope. However, when writing to a property of an object that is on a parent scope (as you do in method 3), the write occurs on the parent object as you expect.
Try
angular.extend($scope.person, $scope.personCopy);
instead of
$scope.person = $scope.personCopy;

Categories