Angular: $observe an element's text - javascript

I already know that you can use attr.$observe within a directive to watch the attributes if they change.
Is there an equivalent to watch the text within an element?
<div my-directive ng-bind="myText || 'Watch me!!!'">Watch me!!!</div>
In the above example, I'd want to watch the text "Watch me!!!". I'd much prefer to do this rather than $watch what is being bound to the directive because of scoping issues.

Watching the DOM for changes is EXPENSIVE, also there is no built in way within Angular to do so. Watching the scope for changes is the correct way to do this.
If you are having issues with scoping then perhaps, with more I formation,that is something we could help with as well.

It's possible to watch DOM changes, here's an example:
http://jsfiddle.net/kihu/t7zr71ma/5/
The trick is to pass a function returning anything you want to watch, e.g.:
scope.$watch(function () {
return element.text();
}, handleChange)
But I think #Enzey is right, you should avoid watching the DOM, instead you should bind data from angular scope and watch it.

Related

angular unit testing a $watch on an $attr

From my understanding $attr.$observe fires once. So there are times to use $watch on an attribute. I am looking to unit test a directive that requires ngModel
scope.$watch(attr.ngModel, function (newValue) {
minlength = parseInt(attr.minLength);
scope.minLengthValidator(newValue);
});
Since this is using scope in the Link function it seems like I could call $digest.
My mocked attribute starts like so...
html = angular.element("<input ng-model=\"myUnit\" min-length=\"3\">");
I am not sure if I can just redefine element.attr('min-length') inside of my spec and run a $digest or if there is a more complex approach since the the watch is passing a new value.
the rest of my mock set up is like so
$rootScope = $rootScope.$new();
element = $compile(html)($rootScope);
$rootScope.$digest(element);
controller = element.controller('ngModel');
scope = element.scope();
I have not tested a $watch on a attribute before so any direction that points me towards solving this would be much appreciated.
scope.$watch(attr.ngModel, ...) will create a watcher on myUnit scope property. Once the watcher is created, it isn't bound to ngModel attribute value.
It can be tested as any other scope watcher:
scope.myUnit = ...;
$rootScope.$digest();
expect(scope.minLengthValidator).toHaveBeenCalledWith(...);
From my understanding $attr.$observe fires once.
No, $attrs.$observe observer will fire on each attribute change, it is preferable to $scope.$watch. As the manual states,
Use $observe to observe the value changes of attributes that contain
interpolation (e.g. src="{{bar}}"). Not only is this very efficient
but it's also the only way to easily get the actual value because
during the linking phase the interpolation hasn't been evaluated yet
and so the value is at this time set to undefined.
The problem is that attributes belong to DOM and can't be tested cleanly. For controller specs $attrs local dependency can be mocked, but it isn't possible in directive specs. For testability reasons it is preferable to bind attributes to scope properties and test the scope only with no $attrs involved.

$watch vs. ngChange

Say that you want to do something when a property of $scope changes. And say that this property is bound to an input field. What are the advantages/disadvantages of using $watch vs. using ngChange?
html
<input ng-model="foo" ng-change="increment()">
<p>foo: {{foo}}</p>
<!-- I want to do something when foo changes.
In this case keep track of the number of changes. -->
<p>fooChangeCount: {{fooChangeCount}}</p>
js
// Option 1: $watch
$scope.$watch('foo', function() {
$scope.fooChangeCount++;
});
// Option 2: ngChange
$scope.fooChangeCount = 0;
$scope.increment = function() {
$scope.fooChangeCount++;
};
http://plnkr.co/edit/4xJWpU6AN9HIp0OSZjgm?p=preview
I understand that there are times when you need to use $watch (if the value you're looking to watch isn't bound to an input field). And I understand that there are times when you need to use ngChange (when you want to do something in response to a change in an input, but not necessarily in response to a scope property change).
However, in this case both accomplish the same thing.
My thoughts:
ngChange seems cleaner, and easier to understand what's happening.
$watch seems like it might be slightly faster, but probably negligible. With ngChange, I think Angular would have to do some extra work in the compile phase to set up the event listeners, and perhaps extra event listeners decrease speed a bit. Regardless of whether or not you use ngChange, the digest cycle runs on changes, so you have an opportunity to listen for something and call a function in response to changes.
Bottom line - You can achieve with $watch every thing you can achieve with ng-change but not vice-versa.
Purposes:
ngChange - binded to a HTML element
$watch - observing scope's model objects (HTML object models included)
My rule of thumb - if you can use ng-change use it to match your scenario, otherwise use $watch
Why you shouldnt use $watch?
It’s inefficient - Adding complexity to your $digest
It’s hard to test effectively
It's not clean
You have it mostly right. ng-change is very DOM specific and for evaluating an expression when the change event fires on a DOM element.
$watch however, is a lower-level (and more general purpose) utility that watches your view model or $scope. So your watch function will fire every time the user types a key (in the example of an input).
So to contrast, one listens to DOM events, the other watches your data.
$watch adds more complexity do the $digest, making it less efficient. In your case ngChange it's a cleaner and easier solution...
Font: http://www.benlesh.com/2013/10/title.html

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>

Should "ng-src" always be used, even without interpolation ?

In my angularjs project I need to go back and forth using both ng-src and src attributes in my tags.
Some images are static assets and will never change (they may be conditionally displayed) while other images are dynamic and dependent on scope variables.
May I use the mix of ng-src and src whenever I see fit?
I'm asking that because I once read that I should always use ng-src when working with angularjs, but I'm also afraid that I'm going to create bindings and watches that are really not necessary...
always using ng-src ?
I don't see any reason using ng-src without interpolation.
#Vojta Jína says: "watching attributes only makes sense, if you interpolate them"
The documentation says: "Using Angular markup like {{hash}} in a src attribute doesn't work right"
It not says using src in angular.js is not right...!
Same case with ng-href.
using ng-src without interpolation will probably not cause performance issues.
If you hit performance I guess ng-repeat is guilty.
does ng-src always creates bindings?
Actually the source code is pretty straightforward
https://github.com/angular/angular.js/blob/v1.2.7/src/ng/directive/booleanAttrs.js
ng-src will always $observe your attributes.
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
var normalized = directiveNormalize('ng-' + attrName);
ngAttributeAliasDirectives[normalized] = function() {
return {
priority: 99, // it needs to run after the attributes are interpolated
link: function(scope, element, attr) {
attr.$observe(normalized, function(value) {
if (!value)
return;
attr.$set(attrName, value);
// on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
// we use attr[attrName] value since $set can sanitize the url.
if (msie) element.prop(attrName, attr[attrName]);
});
}
};
};
});
As for $observe, from the documantation:
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Or more Simple, if there is no interpolation there is no dirty checking.
AFAIK, all performance issues angular.js has with (lots of) bindings is when it does dirty checking.
Looking further inside compile.js:
// no one registered attribute interpolation function, so lets call it manually
If there is no interpolation we end up invoking the callback only once.
In the case of ng-src you can see above the callback that $observe registers.
If there is an interpolation then angular will register a $watch (dirty checking) like so $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
Conclusion
use src when there is no interpolation
use ng-src when there is an interpolation
using ng-src without interpolation will end up running the callback only once. means it has subtle impact to performance.
Another issue is that images will not start loading until angular is bootstraped or in case you use routes (who's not?) until your views are compiled. This could a have a significant impact on load times.

Angular multiple instances of same directive, scope is not isolated

I have a problem when creating multiple directives with isolated scope: when I change something in 1st directive it also makes changes in all other directives.
Here is a working example: http://plnkr.co/edit/drBghqHHx2qz20fT91mi?p=preview
(try to add more of Type1 'Available notifications' - a change in 1st will reflect in all other directives of Type1)
I found some solutions to similar problems here but they don't work in my case. Also found a working solution with mapping 'subscription' data to local scope variables in directive (app.js, line 76) but I think there should be a more general way to do this right?
In your directive 'notificationitem' you have the following code, keep it in mind as i explan:
// if all variables are mapped in this way than works
//$scope.enabled = $scope.subscription.enabled;
The reason why all of the 'isolated' scopes are updating is because of this code in your scope declaration in the same directive (notificationitem):
scope: {
subscription: '=',
index: '#'
},
The equal sign on subscription is angular's way of saying "Whenever the current scope updates, go to the parent and update that value as well." This means whenever you update your 'isolated' scope, it updates the parent scope as well. Since all of these isolated scopes are binding to the parent, they will change as well.
Since you want the subscription.value to be the default value of that text field, you will need to do exactly what your commented code is doing:
scope.value = scope.subscription.value;
This will create an isolated value inside of the isolated scope. When scope.value changes, scope.subscription.value will not. All of the text fields now have their own 'value' to keep track of.
Check out this article for information on directive bindings: http://www.ng-newsletter.com/posts/directives.html
Also, another way to get the default value would be to inject your service into the directive, if you don't like the above solution. Hope this all helps.

Categories