Using ng-repeat with directive causes flaw in watch - javascript

I have made a new directive that is basically a new carousel that extends the angular UI carousel bootstrap. This new carousel will display multiple divs in one frame. My new directive accepts any data in an array and a custom html template for each data.
However, if I use my carousel with a directive, there is a weird behaviour that I am seeing with watch inside the directive. Everything works fine, but the watch inside my directive is always getting the same value for newVal and oldVal. What I mean is this, here's my carousel code:
<slide ng-repeat="frame in ctrl.frames">
<div ng-repeat="divs in frame.divs">
<custom-directive data="divs"></custom-directive>
</div>
</slide>
and inside my customDirective controller, I watch the change of data like this:
$scope.$watch("ctrl.data", function(newVal, oldVal){
if (newVal !== oldVal) {
// data is updated, redraw the directive in my case
// however newVal is always the same as oldVal
}
})
newVal and oldVal is always the same.. I expected the initial state to be oldVal = undefined and newVal will be my new data. However, this is never the case. Data is passed as a two-way binding to carousel and to custom directive (using '=' operator inside the scope of each directive).
Why is this happening? I have investigated this for long and here's my findings:
If I don't use ng-repeat inside my carousel, this will work. oldVal will be undefined and newVal will be my data during the initial state. But why is ng-repeat causing this? I have read lots of article regarding golden rule of prototypical inheritance, that says ng-repeat will create new childScope that hides/shadows the parent, but that only happens to primitive object and I am passing an array to my data.
I need to use ng-repeat in my carousel directive.. so I need to know why ng-repeat is causing this.. any suggestions?
UPDATE:
Reproduced the problem in Plunkr here. As you can see, oldValue is always the same as newValue (I expected the oldValue to be undefined in the beginning)

When you register the $watch in your link function, Angular has already processed the bindings during the preLink phase, hence you will never see undefined the first time your watcher is executed (that initialiation call is the only moment on which oldVal and newVal are potentially the same. If the watcher was registered before the bindings resolution, the oldValue would be undefined)
If you really want to see it, you could override the compile phase and add a custom preLink method (the default link being the postLink).
But I really doubt that you want to do that. Why is it a problem to not have undefined the first time? You should try to explain the real problem you are facing.
Also, note that if divs that you pass to your directive is an array, you should use scope.$watchColleciton instead of scope.$watch in order to detect changes in the array elements instead of change of the whole array pointer.

I think the problem you are having is just a misunderstanding of how $watch works.
$watch is expected to initialize with equal values. See the documentation here. Specifically:
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
So in other words, your check for if they are equal is so you don't detect the initial call
In your provided Plunker, if you need to do some initialization code, you can do two things:
You can check if they are equal in the $watch function, and if they are then that is the initial call with their initial values
Or, outside of that function in the link function, the values are their initial values there (since the link function is equivalent to post-link, which means scope values have already been linked) so you can put your code there
Forked your Plunker here. Notice I moved the alert outside of the $watch, and the value is still valid
EDIT:
The reason you see a difference when it is not in the ng-repeat and it is set up like your commented out code in the Plunkr is due to you adding data in a $timeout. When the page initially loads, below are what the two types render as:
<a1 prop="data[0]"></a1>
HTML looks as it was written. data=[]. directive element exists, calling link with data[0]=undefined. $watch called with prop=undefined
<!-- ngRepeat: element in data track by $index -->
HTML is simply a comment. waiting for data to be populated. No directive element exists, which means link is not called
When you add items to data after the timeout, they look like this:
<a1 prop="data[0]"></a1>
Same as above. data[0] is now defined so prop is defined
<div ng-repeat="element in data track by $index" class="ng-scope">
<a1 prop="element" class="ng-isolate-scope"></a1>
</div> (x3)
Page now has directive elements. calling link function on each with data now filled. $watch called with prop values linked

Related

Updating parent's object data from child controller

I have a object $scope.object in parentCtrl, and I have a isolated scope directive. Inside directive, I have a functionality wherein I need to make an API call and fetch one of the property of $scope.object (this property is an array of object) and replace this property in original $scope.object.
I have tried various ways of doing it but somehow I am not exactly getting at it. I have tried updating it using $scope.$parent.object from directive's controller.
After this I tried sending the fetched array in to the parent's controller and replacing it there.
Once the $scope.object gets updated, the directive should run as the directive's template is binded with properties of $scope.object.
The interesting thing is that the view gets updated with new data but the data in original object somehow again becomes null.
I guess, there is some issue with the way my directive is called as the same directive is called nestedly. As shown below:
<li ng-repeat="t in A" ng-if="t.selected">
<div directive> <div>
</li>
where, A is the fetched property and t.selected is true. Now in the fetched data t.selected is true for two elements of array, while the directive is called 4 times, twice for each element. In the first iteration the data remains intact in parent object, while in 2nd iteration, data becomes null
I want to know, how do I update the data of parent object permanently
Directive:
<li ng-repeat="(key, t) in fetch.data>
<div layout="row" layout-wrap directive target="t" ></div>
</li>
and this target is double way binded in directive's scope with parent's scope

When exactly is ng-checked called

While using AngularMaterial, I have ng-checked like this:
<md-list>
<md-list-item ng-repeat="option in options">
<p> {{ option }} </p>
<md-checkbox class="md-secondary" aria-label="{{$index}}" ng-checked="exists($index)" ng-click="toggle($index)"></md-checkbox>
</md-list-item>
</md-list>
And my exists function:
$scope.exists = function (optionNum) {
console.log('Inside $scope.exists. option: '+optionNum);
};
My timer:
function updateTimer() {
var onTimeout = function(){
mytimeout = $timeout(onTimeout,1000);
}
var mytimeout = $timeout(onTimeout,1000);
}
With this, the $scope.exists function is getting called every second. Can someone please explain how ng-checked and $timeout related ? and how to avoid this ?
Reason in one word is: digest cycle. Since your function is bound to the view, every time digest cycle happens those expressions gets evaluated as a part of dirty check to make sure if respective DOM needs to be updated or not. This is nothing to do with angular material alone, it is the core angular implementation. Now in your case you are calling $timeout infinitely which means after each timeout execution digest cycle happens to perform dirty check.
Now what you have is fine, but whenever you bind a function to DOM (as a part of view binding, interpolation or property state attributes or even DOM filters - of course events are fine) you should be aware of the fact that you do not perform extensive operation in that function accidentally or intentionally as the app grows, it will slow down the entire app, and will be hard to refactor and diagnose when the app grows larger and problems starts happening. As much as possible bind to a property instead of a function. Note that even if you bind a property still angular $parse creates a getter on it and adds it to the $$watchers queue to be dirty checked every digest cycle, but difference is that it is a simple getter function.
So basically for instance in your case you could bind ng-checked to property
..ng-checked="doesExist"
and set the property doesExist whenever it needs to be updated. So with this instead of checking for the existence every time, you explicitly set the respective property when a respective event happens. That makes the logic explicit as well.
ng-checked, like many of angular's directives are based on watches. Anytime a digest cycle is called, it evaluates all of the watchers (which the function you are using is one of). So every time $timeout evaluates it is starting a new $digest cycle and evaluating all of the watchers. This is part of the "magic" that keeps the view updated with all of the data in your controllers and directives.
Watchers can become a performance issue if you make your functions complex, or make TONS of watchers. It is generally best to have simple logic that returns true or false very quickly and to avoid setting watches on everything.

Data binding in directive isolated scope

I want to create a directive with isolated scope, but I'm not able to get it working.
jsFiddle
I want to isolate age model in a directive scope. I want to perform some business logic on that model and then set that model to parent binding. I hope the fiddle is explanatory.
I am also adding a button to the template which when clicked should invoke a submit function:
<button ng-click="submit()">click me</button>
It seems the button is working fine, but why is $scope.$watch() is not begin triggered? In a normal situation, if I change the view value it will automatically update the model value. But now it isn't.
$watch requires a dollar sign, and you pass either a function or a string that is evaluated on your scope, i.e.:
$scope.$watch('age', function(value) {
There are many more errors in your code, for instance you don't have a declared variable called 'age' so this line will reference window.age and give you an error because it is undefined, you need to say $scope.age I think:
age = age+10;
It just looks like your updated fiddle is a playground, hope these point you in the right direction. I'd recommend going through the egghead.io angular videos.

Angular ngModel binding on first level method of $scope

This is not exactly a question with some code, but more for my understandings. So please forgive me if this isnt appropriate here..
I was playing with a simple checkbox with an ngModel assigned:
<input type="checkbox" ng-model="someFlag"/>
I expect this to bind the boolean of the checkbox to $scope.someFlag (if controller and everything else configured properly).
And yes, it does work. But there are times where I found that this is not working. Case: When I try to do something when the someFlag changes (for example in a $watch) the value is not really binded.
But then I came accross something a collegua at work mentioned once:
Use a wrapper object
Doing it like that works now without any problems:
<input type="checkbox" ng-model="wrapperObject.someFlag"/>
Watching $scope.wrapperObject.someFlag works as expected.
Now the question: Why??
When ngModel directive is executed, it reads attribute's ng-model value (in this case, "someFlag") and saves it in it's local function scope (not to confuse to angulars $scope). But since a boolean in javascript is a primitive, you cannot pass it by reference, only by value. That means only the $scope.someFlag's value (true or false) get's copied to ngModel, and not the way of accessing and modifying $scope.someFlag from ngModel.
When you use wrapper object, it is passed by reference, meaning, it is the same thing both in $scope.wrapperObject and in ngModel's local function scope, because it points to the same memory address behind the scenes.
I'm not sure if this explanatory enough, but when working with primitive values in angular, you must keep in mind the difference between passing by reference and by value and that when primitive's change, other parts of application might not know this.
In my understanding you want to pass Boolean value if check box checked to controller
try this:
In your html:
<input type="checkbox" ng-model="value"
ng-true-value="YES" ng-false-value="NO"> <br/>
in your controller:
$scope.value is gives Boolean value//
This is because scope inheritance. A nice article about angularjs scope and inheritance
I guess the $watch logic you mentioned might have been triggered from inside a ng-repeat or a ng-if. Remember angularjs creates a new scope variable for each object inside ng-repeat and ng-if.
<input type="checkbox" ng-model="someFlag"/> //This is a primitive $scope.someFlag
<input type="checkbox" ng-model="obj.someFlag"/> //This is an object $scope.obj.someFlag
<div ng-repeat="opt in options">
Here a new child scope - $scope.somechild is created for the opt variable.
So if primitive variable someFlag is referenced here it will be taken as $scope.child.someFlag. so any update here will not update the parent $scope.someFlag.
But if an object obj.someFlag is referenced here by object inheritance the compiler will try to find obj.someFlag in child - $scope.child as it is not present it will search in parent $scope as it is present this will now refer to $scope.obj.someFlag and any modification done here is done in the actual parent scope.
This is part of the mantra: "there should always be a dot in your model" by Misko Hevery, one of the fathers of Angular. You can, and probably should, watch this video: https://www.youtube.com/watch?v=ZhfUv0spHCY&feature=youtu.be&t=32m51s
ngModel is a directive that does two-way-data-binding. Binding to a primitive (Boolean in this case) will make the setter to set it on the current scope, rather than on the scope that's defined which could interfere with other scopes.
In your scenario, in a parent scope we have your $scope.someFlag = true. In a child scope we then have your input:
<input type="checkbox" ng-model="someFlag"/>
That will work initially but once the user changes the value, the someFlag will be created on the child scope and the bindings will read and write from that value from now on.
I hope this is, somehow, clear.
Note that this only happens with two-way-data-binding directives and not regular ones such as ngDisabled or ngHide

AngularJS: Using JS functions inside directives - bad practice?

I have this part of code inside ng-repeat:
<div ng-show="parameter == 'MyTESTtext'">{{parameter}}</div>
where parameter is some $scope string variable ...
I wanted to see if its possible to check (inside ng-show for instance) whether parameter contains a substring.
You could do this with:
<div ng-show="parameter.indexOf('TEST') != -1">{{parameter}}</div>
which seems to be working; it displays every parameter that contains 'TEST' keyword.
I was wondering:
is this a correct way of doing this within AngularJS app?
Is it OK to use javascript built in functions like that?
EDIT:
parameter is actually formed like this: (and is thus not a $scope variable as I said above, sorry)
<div ng-repeat="(parameter,value) in oneOfMyScopeArrays">
UPDATE
Since you're dealing with strings in ngRepeat and not objects, there's no place to set flag to in your data elements. In this case I would advise using a custom directive. I do not agree with Darryl Snow's opinion that directive in this case is redundant. With directive (as it was the case with flag in controller) you can evaluate parameter once instead of doing so in every $digest cycle. Furthermore, if you decide to implement same functionality in other template, instead of copying the expression around, which is redundant, you'd reuse same directive. Here's a quick idea of such directive:
.directive('parameter', function() {
return {
link: function($scope, $element, $attrs) {
$attrs.$observe('parameter', function(parameter) {
if (parameter.indexOf('TEST') == -1) {
$element.hide();
} else {
$element.text(parameter);
$element.show();
}
});
}
}
});
Template:
<div parameter="{{parameter}}"></div>
This directive even sets up one watcher less per parameter comparing to your original solution, which is better performance wise. On the other hand, it disables two-way binding (parameter text is rendered once), so it won't work in case you want to edit parameter string in place.
ORIGINAL ANSWER
Is it correct way? Technically yes, because it works. Is it OK? Not so much because of several reasons:
Performance. Everytime $digest loop runs (it might run quite a lot, depending on interactivity of application), it has to process every such expression. Therefore string parameter.indexOf('TEST') != -1 has to be parsed and evaluated, which means calling .indexOf up to several times after each interaction, for example click on element with ngClick directive. Wouldn't it be more performant to test this assumption parameter.indexOf('TEST') != -1 once in Controller and set a flag, e.g.
$scope.showParameter = parameter.indexOf('TEST') != -1
In template you would write
<div ng-show="showParameter">{{parameter}}</div>
Model logic in template. It's hard to tell the actual reasoning from your example when the parameter should be visible, but is it up to the template to have this logic? I think this belongs to controller, if not model to decide, that your view layer would be decoupled from making assumptions about how the model actually works.
Yes, perfectly ok I think. You could write a separate directive of your own that does the same thing - it may look a bit tidier but it's ultimately redundant when angular comes with ng-show already built in, and it means a slight additional payload to the user. You could also do a $scope.$watch on parameter and set another scope variable for ng-show, but that just moves the mess from your view to your controller.
$scope.$watch('parameter', function(){
if(parameter.indexOf('TEST') != -1)
$scope.showit = true;
});
and then in the view:
<div ng-show="showit">{{parameter}}</div>

Categories