Using a watch inside a link is causing an infinite digest cycle. - javascript

I'm trying to write a directive that associates a score with a color.
I've made an attempt already, and the Plunker is here. The directive itself is here:
.directive('scorebox', function () {
function link ($scope, $elem, $attr) {
var one = 1;
$scope.$watch('[score,ptsPossible]', function (newValue) {
pctScore = newValue[0] / newValue[1]
if (pctScore <= 0.4) {
rating = 'low';
} else if (pctScore <= 0.6) {
rating = 'med';
} else if (pctScore <= 0.8) {
rating = 'high';
} else if (pctScore == 1) {
rating = 'perfect';
}
$elem.removeClass();
$elem.addClass('scorebox');
$elem.addClass(rating);
$elem.text(newValue[0] + "/" + newValue[1]);
});
};
return {
restrict: 'E',
scope: {
score: "=",
ptsPossible: "="
},
link:link
}
})
I've got a couple of problems.
First, it's pretty obvious to me that I'm not supposed to do a $watch inside a link function. I'm creating an infinite digest cycle, and that's not good. I'm still not sure why, though.
I'm not manipulating the DOM correctly. Even though I'm calling $elem.removeClass(), it's not working--the element retains any classes it had before.
What is the right way to do this?

As #miqid said, no need to $watch both score and ptsPossible, since you only want to react when score changes (at least in this situation you are presenting).
The problem here, is you are using jqLite's removeClass function instead of jQuery's. If jQuery is not included before Angular in the code, Angular will instead use jqLite functions, which is like a smaller, much simpler version of jQuery. It is also, slightly different. jQuery's removeClass(), will remove all classes is no parameter is passed. jqLite will not do the same, it will just remove those classes that you pass as parameter.
You never included jQuery at all, so that's what's happening. Here is the edited Plunker. You can check jQuery is now included in the top, and everything works as expected. And also the $watch is much simpler.

Just a suggestion to get things working:
There's no need to $watch both score and ptsPossible since the latter never changes after the value is loaded from its corresponding attribute value. You also have access to scope variables inside the $watch callback function.
That's unusual as I would've expected your removeClass() to work as well. You could instead try removeAttr('class') here in the meanwhile.
Here's a Plunker with the suggested changes.

You need to use $watchCollectionand not $watch. You are passing in an array to $watch which is how $watchCollection expects.

Related

How do I tidy up ng-class?

I've got a scenario whereby I have to detect if certain fields in an Angular form are valid and dirty.
I am currently doing this using ng-class which works perfectly. However, I am ending up with a massive expression which looks really messy and sloppy in the html. Below is an example:
data-ng-class="{'component-is-valid' :
form.firstName.$valid && form.firstName.$dirty && form.lastName.$valid && form.lastName.$dirty && form.emailAddress.$valid && form.emailAddress.$dirty && form.mobileNumber.$valid && form.mobileNumber.$dirty}"
As you can see, this is quite long.
Is there anyway I can extract this so that I retain the flexibility of ng-class but also free up my DOM?
Make a function on your scope that accepts your form object or input fields and returns the boolean you're describing above: ng-class="{'component-is-valid': checkValidity(form)}"
You can check the whole form validation in one hit with $ctrl.form.$valid instead of checking all, as Keegan G correctly states.
That said, there can be cases where your ngClass logic get's quite large and unreadable.
An alternative approach I often adopt is to move all logic to the controller. e.g.
Template:
<div ng-class="$ctrl.componentClasses()"></div>
Controller:
controller: function() {
var vm = this;
vm.isValid = function() {
// do all your checking here, ultimately it should return a bool
return [Boolean];
}
vm.componentClasses = function() {
return {
'my-class-name': vm.isValid()
}
}
}

Angular ngClass is not updating my classes even though my condition change as expected

I have something like this in a template I am creating
<div ui-view id="app" class="nng-3" ng-class="{ 'app-mobile': app.isMobile, 'app-navbar-fixed': app.layout.isNavbarFixed, 'app-sidebar-fixed': app.layout.isSidebarFixed, 'app-sidebar-closed': app.layout.isSidebarClosed, 'app-footer-fixed': app.layout.isFooterFixed }"></div>
The values app.layout.isNavbarFixed, etc are initialized with either zero or one, and for the first time the page loads the appropriate classes are inserted into my div. Any change after that though, by means of a button that sets those values, is not reflected on my class attributes by ng-class.
If I directly print those variables in my template, eg. {{app.layout.isSidebarFixed}} I can see them changing from true to false and vice versa, but ng-class will not update or remove any new classes.
I am not sure where to begin and look for the solution for this since with my limited knowledge I cant spot any obvious mistake immediately. Does anyone have any idea on what causes this issue?
A workaround of mine is to manipulate a model variable just for the ng-class toggling:
1) Whenever my list is empty, I update my model:
$scope.extract = function(removeItemId) {
$scope.list= jQuery.grep($scope.list, function(item){return item.id != removeItemId});
if (!$scope.list.length) {
$scope.liststate = "empty";
}
}
2) Whenever my list is not empty, I set another state
$scope.extract = function(item) {
$scope.list.push(item);
$scope.liststate = "notempty";
}
3) I use this additional model on my ng-class:
ng-class="{'bg-empty': liststate == 'empty', 'bg-notempty': liststate == 'notempty'}"
Update*: Also you can add any other states if needed and use at ng-class like:
ng-class="{'bg-empty': liststate == 'empty', 'bg-notempty': liststate == 'notempty', 'bg-additional-state', liststate == 'additional-state'}"
Because you know, an initially empty list state is not equal with a list which is emptied by command.
Probably the ng-class implementation is not considering "0" to be "false" as you expect, because it's doing an strict comparision with ===.
Try expressing the conditions like this:
ng-class="{ 'app-mobile': app.isMobile == 0, 'app-navbar-fixed': app.layout.isNavbarFixed == 0, ...
Tried your variant. Have everything working. Please, check if your button click event is on $scope and AngularJS knows, that values changed.
For example, if function triggered by native DOM Event (some jQuery table updated or something) than you should use $apply function, to reflect changes on scope. Something like this:
$scope.eventHandler = function(e) {
$scope.$apply(function(){ $scope.someProp = e.value;}
}
In the mean time, check this jsfiddle
Update:
Please check this jsfiddle out. This works in AngularJS 1.4.8, but doesn't in other, that support jsfiddle. From what I know, it's not really a best idea to do an assignment inside of expression, the controller is meant for this thing.

angular, ng-class with function inside repeat

I'm trying to put a class on an input if it meets certain requirements and am having problems
it looks like so -
ng-class="'isPartial': canPartial($index)"
This is inside a repeat, the function it's referring to looks like so
$scope.canPartial = function(index) {
var needsMet = _.reduce($scope.allAccounts[index].schools, function (memo, schools) {
return memo + (schools.selected ? 1 : 0);
}, 0);
console.log(needsMet);
return (needsMet === $scope.allAccounts[index].schools.length);
};
so it's using underscore.js to check if all its children are checked. I know the function works correct, however my issue is passing it as the condition for the ng-class. So if it returns true it will add the class. I'm getting a $parse.syntax error and I cannot seem to figuire out why because I se other examples of ng-class using a function. Perhaps it's because I'm trying to pass the $index, however it is inside a repeat, but I don't know if that causes an issue.
Any help would be much appreciated. Thanks!
Your ng-class expression is invalid.
Change your ng-class declaration in order to take an object as a value:
ng-class="{'isPartial': canPartial($index)}"

Providing dynamic value to directive without causing reevaluation in AngularJS

I want to use an AngularJS directive as HTML element, and provide it with two values: One is retrieved by iterating through a collection, and the other one is calculated from that value:
<mydirective myval="val" mymagic="getMagic(val)" ng-repeat="val in values" />
It works as expected when the getMagic(val) function always returns the same value. In my case the result values are Arrays, and as each call results in a new value, I will end up in a
[$rootScope:infdig] 10 $digest() iterations reached. Aborting!
error. You can find a fiddle with my example here (if the function is defined like it is done in the commented line, it works).
Is there any way to not reevaluate or "watch" the mymagic parameter? I do want the value to be recalculated when the values collection changes, but the function should not be called apart from that.
Is there any way to achieve this?
Try this
(UPDATED JSFIDDLE)
http://jsfiddle.net/yogeshgadge/6T8mr/6/
notice the change - your getMagic() got called multiple times as the values returned also triggered the change and cause inifinite....10 digest
app.directive("mydirective", function () {
return {
restrict: "E",
transclude: false,
scope: {
myval: "=",
mymagic: "&" //passing the function
},
template: "<li>{{myval}} -- {{mymagic()}}</li>",
///mymagic() executing this function here
replace: true
};
});
I think you are concerned that getMagic(val) is being called multiple times when the page is being rendered. What's going on is that the $digest cycle is running a few times to get the page completely rendered. This is expected behavior. See more information in the Angular guide to scopes.
What you can do is generate the magic numbers in the controller and attach it to a scope. And then any time the array or magic numbers change, you explicitly call $scope.$apply().
Something like this might work:
app.controller('Controller',
function ($scope) {
var getMagic = function(val){
return val + 1;
};
$scope.values = [3,7,1,2, 100];
$scope.magic = recalculate();
// EDIT: every time $scope.values changes, recalculate $scope.magic:
function recalculate() {
return $scope.values.map(getMagic);
}
$scope.$on('somthing-changed', recalculate();
});
Now, you will still need to ensure that every time either the values or the magic arrays change, you explicitly call $digest(). This is not as elegant as using a $watch() expression, but it will be more performant since you are not reevaluating the magic array more often than you need to.

AngularJS filter causes IE8 to not render two-way binding

I have a bizarre issue with IE8 where if I try to render a $scope variable in a template via AngularJS's two-way data binding, it won't replace {{child.name}} with the proper value. This surely has something to do with the inefficiency of the following filter:
filter('truncate', function() {
return function(name) {
// It's just easier to use jQuery here
var windowWidth = $(window).width(),
nameElement = $('a:contains("' + name + '")'),
truncPnt = Math.floor(name.length * 0.9);
while (nameElement.width() > windowWidth * 0.75 && truncPnt > 6) {
truncPnt = Math.floor(truncPnt * 0.9);
name = name.substring(0, truncPnt);
nameElement.text(name + ' ...');
}
return name;
}
});
I then use this filter with an ng-repeat with:
<a class="entity-name" href="{{child.url}}" title="{{child.name}}" ng-cloak>{{child.name|truncate}}</a>
The overall goal is to have the variable passed into the filter truncated down depending on the width of the screen, replacing any truncated characters with " ...". I'm fairly confident this filter is the cause since I have a similar function that gets called on a .resize() of the $(window) handler, and if I were to take IE8, and resize the browser window, it causes the {{child.name}} to render as the proper value, but only if I resize the browser.
UPDATE:
So I've gotten rid of the above filter, and replaced it with a very similar directive. This is my first attempt at creating a custom directive, so I'm fairly certain it could be done better, minus the obvious flaw that I cannot seem to work around currently. The directive is as follows:
.directive('truncate', function() {
return {
restrict: 'A',
replace: true,
template: '<a class="entity-name" href="{{child.url}}" title="{{child.name}}">{{child.display}}</a>',
link: function(scope, element, attr) {
var widthThreshold = $(element[0]).parent().parent().width() * 0.85;
scope.$watch('child', function(val) {
var elementWidth = $(element[0]).width(),
characterCount = scope.child.name.length;
while ($(element[0]).width() > widthThreshold || characterCount > 5) {
characterCount--;
scope.child.display = scope.child.name.substring(0, characterCount) + ' ...';
}
});
}
}
});
And I replace the partial to simply:
<a truncate="child"></a>
The differences in this as opposed to the filter are as follows (minus the obvious filter vs. directive):
Replace windowWidth with widthThreshold, identifying the value by chaining jQuery's .parent() twice (essentially it's a more accurate value when getting the width of the parent (x2) element instead of the window).
Added an additional key to child called display. This will be a truncated version of child.name that is used for display, instead of using jQuery's .text() and just rendering using a truncated child.name.
truncPnt becomes characterCount (trying to remember not to abbreviate variables)
The problem now becomes that jQuery is freezing up the browser, until I kill the javascript (if prompted). Firefox may display it, Chrome has yet to not hang, and while I've yet to test in IE, I'd imagine worse than the former.
What can be done to properly get the value of two parents above the main element in question, and truncate child.display so that it will not wrap/extend past the parent div?
UPDATE 2:
I decided to ditch the thought of primarily DOM-based calculations in favor of mathematics, accounting for the width of the parent div, font size, and a ratio of God knows what. I seriously plugged away at a formula until I got something that consistently gave similar results no matter the font size. Media queries do impact the font-size CSS of the string in question, so I needed to account for that or else have some drastic differences in the length of the truncated string between different font-size's:
.directive('truncate', function() {
return {
restrict: 'A',
replace: true,
// {{child.display}} will be a truncated copy of child.name
template: '<a class="entity-name" href="{{child.url}}" title="{{child.name}}">{{child.display}}</a>',
link: function(scope, element, attr) {
var widthThreshold = $(element).parent().parent().width() * 0.85,
// get the font-size without the 'px' at the end, what with media queries effecting font
fontSize = $(element).css('font-size').substring(0, $(element).css('font-size').lastIndexOf('px')),
// ... Don't ask...
sizeRatio = 29760/20621,
characterCount = Math.floor((widthThreshold / fontSize) * sizeRatio);
scope.$watch('child', function(val) {
// Truncate it and trim any possible trailing white-space
var truncatedName = scope.child.name.substring(0, characterCount).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
// Make sure characterCount isn't > the current length when accounting for the '...'
if (characterCount < scope.child.name.length + 3) {
scope.child.display = truncatedName + '...';
}
});
}
}
});
Interestingly enough, I believe I came full circle back to Brandon Tilley's comment regarding modifying the DOM versus modifying a property in the scope. Now that I've changed it to modifying a property, it would probably better serve in a filter? What is typically the deciding factor for whether or not this sort of manipulation should be handled in a filter versus a directive?
I refer to the documentation:
Directives
Directives are a way to teach HTML new tricks. During DOM compilation directives are matched against the HTML and executed. This allows directives to register behavior, or transform the DOM.
http://docs.angularjs.org/guide/directive
Filters
Angular filters format data for display to the user. In addition to formatting data, filters can also modify the DOM. This allows filters to handle tasks such as conditionally applying CSS styles to filtered output.
http://docs.angularjs.org/guide/dev_guide.templates.filters
I would use filters only for the purpose of changing the format of data and nothing else. To be honest I believe using a filter for your purpose is appropriate. And as the docs say a filter can modify the DOM I don't see a reason why u should use a directive at all, a filter seems to be what you are looking for. (Apart from the fact that a bug may force you to use a directive)

Categories

Resources