AngularJS : Binding a variable in template to increase performances - angularjs

Here is a code sample I use:
<div ng-class="{alert: ((elements|filter:{hasAlert: true}).length / elements.length) > maxPercentAlerts}">
{{(elements|filter:{hasAlert: true}).length}}
({{Math.floor((elements|filter:{hasAlert: true}).length * 100 / elements.length)}} %)
</div>
As you see, I need to filter my 'elements' array 3 times. I would like to use this kind of following code to increase perfs:
(this is just an example of what I need, not real code)
<div some-ng-prop="alertCount=(elements|filter:{hasAlert: true}).length"
<div ng-class="{alert: (alertCount / elements.length) > maxPercentAlerts}">
{{alertCount}}
({{Math.floor(alertCount * 100 / elements.length)}} %)
</div>
I've tried to handle it with the 'ng-init' attribute: it worked great... But when my model changes, the values are not updated.
Is there a way to do that ?
I've tried to be clear, but please ask for details if you don't understand what I mean.

Here is a plunker: http://plnkr.co/edit/bNjSnee5UwVjp6wHE6RK
I created a directive:
I use $parse rather than $eval for optimizations.
You provide a collection to watch and an expression to run on each change.
It works like ngInit only it updates when the collection is dirty.
I chose general names for attributes, you can change it to what you like.
directive:
app.directive('watchCollection', function($parse){
return {
compile: function(tElm,tAttrs){
if(! tAttrs.assign) return;
var assignFn = $parse(tAttrs.assign)
return function(scope,elm,attrs){
scope.$watchCollection(tAttrs.watchCollection , function(val){
assignFn(scope);
})
}
}
}
})
html:
<div watch-collection="elements"
assign="alertCount=(elements|filter:{hasAlert: true}).length">

Stewie suggested it was a duplicate, I think it's not but it made me try the following code which works great :
<div ng-class="{alert: (alertCount / elements.length) > maxPercentAlerts}">
{{alertCount = (elements|filter:{hasAlert: true}).length}}
({{Math.floor(alertCount * 100 / elements.length)}} %)
</div>

Related

Convert an express-like string to an expression in Angular view

Assuming that I have an expression-like string in my scope
$scope.expressionString = 'model.key == "anything"'
I want to use this string as an expression in view, can I do that?
In view, I will have something like
<div ng-if="expressionString"></div> but of course, expressionString should be something else instead.
I appreciate any help. Cheers!
You can use $eval to evaluate your expression , there are two ways to do it in your case
Solution 1
<div ng-if="$eval(expressionString)"></div>
Solution 2
In the controller store the evaluated value of the expression like below
$scope.expressionString = $scope.$eval('model.key == "anything"')
and then in the view simply use it without using $eval in the view
<div ng-if="expressionString"></div>
I found the answer, made a parse filter to parse the string and assign it a scope
angular.module('zehitomo')
.filter('parse', function ($parse) {
return function (expression, scope) {
return $parse(expression)(scope);
};
});
And in view
ng-if="expressionString | parse:this"
You cannot use global variables (or functions) in Angular expressions. Angular expressions are just attributes, so are strings and not Javascript code.
Please see this stackoverflow answer once
Although, you can achieve it using a function instead of a variable:
$scope.expressionString = function(toCompare) {
return $scope.model.key == toCompare;
}
and in your view:
<div ng-if="expressionString('anything')"></div>

Compiling a String in Angular Before Displaying

I have the following html:
<div ng-repeat="string in myStrings">
<p>{{string}}</p>
</div>
And a string like this that gets added to $scope.myStrings:
$scope.stringIwantToBeCompiled = 'I want to count to 4 via angular: {{2+2}}';
I would like the string to show 4 instead of the {{2+2}} angular expression.
Am I barking up the wrong tree here by trying to do this via $compile? If not, how is it done? Just putting it in compile fails. Do I absolutely HAVE to do this in a directive?
PLNKR FOR REFERENCE
Not sure what your exact goal is, but I can think of two approaches to accomplish this without compiling:
1) Split up the values like so:
<div ng-repeat="string in myStrings">
<p>{{string}}{{mathValue}}</p>
</div>
in controller:
$scope.mathValue = 2+2;
2) Use a function to return the string (I like using this anytime I'm doing anything binding that is non-trivial):
<div ng-repeat="string in myStrings">
<p>{{stringFunction()}}</p>
</div>
in controller:
$scope.mathValue = 2+2;
$scope.stringFunction = function() {
return 'I want to count to 4 via angular: '+$scope.mathValue;
};
I'm not 100% sure whether you are just wanting to count the number of strings in the myStrings array, or just have the ability to add a count, but given your Plunker, you could do the following:
To simply add two variables, update the following line:
$scope.stringIwantToBeCompiled = 'I want to count to 4 via angular: ' + (2+2);
If you wanted to show the count of the number of strings, swap the order of your scope variable declarations and show the myStrings length
$scope.myStrings = ['I am a string', 'I am another string', 'more strings!'];
$scope.stringIwantToBeCompiled = 'I want to count to 4 via angular: ' + $scope.myStrings.length;
Counting the strings will only give you 3, of course, because there are only 3 strings in the array.
Does that solve it for you?
UPDATE
OK - So I think what you want is the count in the string with an ng-click to correspond to the count correct?
If so, then the following on your ng-repeat would do it...
<p>{{string}} {{$index}} </p>
Using $index gives you the index of the repeating item. You can always add 1 to the $index to make it 1-based instead of zero based:
<p>{{string}} {{$index + 1}} </p>
You can append the angular expresion {{}} to the string like:
$scope.stringIwantToBeCompiled = 'I want to count to 4 via angular: ' + {{stuff or 2 + 2}};
Or use $compile Fiddle example
I really needed to use a directive with $compile like shown here:
app.directive('dynamicAlert', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamicAlert, function(html) {
ele.html(html);
$compile(ele.contents())(scope);
});
}
};
});
http://plnkr.co/edit/bJPEyfkKsertulTN7rWp?p=preview

AngularJS ng-repeat with limitTo

Is there a way to type:
<div ng-repeat="item in items | limitTo:what">
where I can substitute "what" with something that will make it iterate through the whole list of items. (note items.length is not what I am searching for.. or it must be with some ugly if inside the html).
In the source for limitTo there is support for an infinite number (Infinity):
if (Math.abs(Number(limit)) === Infinity) {
limit = Number(limit);
} else {
limit = int(limit);
}
Looks like you should be able to set to Number.POSITIVE_INFINITY.
However, the resulting code would probably be no better than using items.length. And would certainly be less understandable.
you don't need {{ }}, here is the documentation
<div ng-repeat="item in items | limitTo:what">
in the controller
$scope.what = 3; // iterate only 3
This is simple man
<div ng-repeat ="item in items | limitTo:10">
or $scope.what = '10'; and use what in limit.

Ng-Repeat array to rows and columns

Thanks for taking the time to read this, I was wondering how I might be able to use ng-repeat to create a grid like box of options. I would like to take an array repeat nth number of items and then move to the next row or column until all items are listed. e.g.
assuming I had an array like [opt1,opt2,opt3,opt4,opt5,opt6,opt7] I would like to display it like this:
opt1 opt2 opt3
opt4 opt5 opt6
opt7
This is more a styling/markup problem than an AngularJS one. If you really want to, you can do:
<span ng:repeat="(index, value) in array">
{{value}}<br ng:show="(index+1)%3==0" />
</span>
http://jsfiddle.net/JG3A5/
Sorry for my HAML and Bootstrap3:
.row
.col-lg-4
%div{'ng:repeat' => "item in array.slice(0, array.length / 3)"}
{{item}}
.col-lg-4
%div{'ng:repeat' => "item in array.slice(array.length / 3, array.length * 2/3)"}
{{item}}
.col-lg-4
%div{'ng:repeat' => "item in array.slice(array.length * 2/3, array.length)"}
{{item}}
There is another version, with possibility to use filters:
<div class="row">
<div class="col-md-4" ng-repeat="remainder in [0,1,2]">
<span ng-repeat="item in array" ng-if="$index % 3 == remainder">{{item}}</span>
</div>
</div>
If all of your items are in one single array, your best bet is to make a grid in CSS. This article should be helpful: http://css-tricks.com/dont-overthink-it-grids/
You can use $index from ng-repeat to apply the correct class for your column (in this case a 4 column grid):
<div class="col-{{ $index % 4 }}"></div>
If you have a 2 dimensional array (split into rows and columns) that opens up more possibilities like actually using an HTML table.
I find it easier to simply use ng-repeat combined with ng-if and offsetting any indexes using $index. Mind the jade below:
div(ng-repeat="product in products")
div.row(ng-if="$index % 2 === 0")
div.col(ng-init="p1 = products[$index]")
span p1.Title
div.col(ng-if="products.length > $index + 1", ng-init="p2 = products[$index + 1]")
span p2.Title
div.col(ng-if="products.length <= $index + 1")
Between Performance, Dynamics and Readability
It seems putting the logic in your JavaScript is the best method. I would just bite-the-bullet and look into:
function listToMatrix(list, n) {
var grid = [], i = 0, x = list.length, col, row = -1;
for (var i = 0; i < x; i++) {
col = i % n;
if (col === 0) {
grid[++row] = [];
}
grid[row][col] = list[i];
}
return grid;
}
var matrix = listToMatrix(lists, 3);
console.log('#RedPill', matrix);
# Params: (list, n)
Where list is any array and n is an arbitrary number of columns desired per row
# Return: A matroid
# Note: This function is designed to orient a matroid based upon an arbitrary number of columns with variance in its number of rows. In other words, x = desired-columns, y = n.
You can then create an angular filter to handle this:
Filter:
angular.module('lists', []).filter('matrical', function() {
return function(list, columns) {
return listToMatrix(list, columns);
};
});
Controller:
function listOfListsController($scope) {
$scope.lists = $http.get('/lists');
}
View:
<div class="row" ng-repeat="row in (lists | matrical:3)">
<div class="col col-33" ng-repeat="list in row">{{list.name}}</div>
</div>
With this, you can see you get n number of rows -- each containing "3" columns. When you change the number of desired columns, you'll notice the number of rows changes accordingly (assuming the list-length is always the same ;)).
Here's a fiddle.
Note, that you get the ol' Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!. This is because Angular is recalling the matrical function upon every iteration. Allegedly, you can use the as results alias to prevent Angular from reevaluating the collection, but I had no luck. For this, it may be better to filter the grid inside of your controller and use that value for your repeater: $filter('matrical')(items) -- but please post back if you come across an elegant way of filtering it in the ng-repeat.
I would stress, again, you're probably heading down a dark alley by trying to write the logic in your view -- but I encourage you to try it in your view if you haven't already.
Edit
The use of this algorithm should be combined with a Matrical Data-Structure to provide methods of push, pop, splice, and additional methods -- in tandem with appropriate logic to complement Bi-Directional Data-Binding if desired. In other words, data-binding will not work out of the box (of course) as when a new item is added to your list, a reevaluation of the entire list must take place to keep the matrix's structural integrity.
Suggestion: Use the $filter('matrical')($scope.list) syntax in combination with $scope.$watch and recompile/calculate item-positions for the matrix.
Cheers!

AngularJS - ngRepeat and ngModel

I have spent the better part of a few hours writing and re-writing this, and am probably just going to write my own directive here if there isn't an answer.
I have a columnized display of inputs, 10 in each of the 6 columns. I am using 2 ngRepeat directives to display them. I am placing 6 fiddles below with my varied attempts at getting them to work right. The problem is, when I use an array of objects, all the data are updated simultaneously. View Fiddle #1 below to see the example.
Here is a quick snippet of the code, which you can also see on the fiddle page. If anyone has some pointers or a way to get #1, #2, or #6 to work, please let me know!
HTML:
<div ng-controller='EntryCtrl'>
<div class="span2" ng-repeat="a in [0,1,2,3,4,5]">
<div ng-repeat="i in entry.slice($index*10, ($index*10)+10)" ng-class="{'control-group': true, error: !entry[($parent.$index*10)+$index].correct}">{{($parent.$index*10)+$index}}<input type="text" class="span12" id="entry-{{($parent.$index*10)+$index}}" ng-model="entry[($parent.$index*10)+$index].input" /></div>
</div>
</div>
Javascript:
var myApp = angular.module('myApp', []);
myApp.controller('EntryCtrl', ['$scope', function($scope) {
$scope.entry=filledArray(60,{input:'',correct:true});
}]);
function filledArray(len, val) {
var rv = new Array(len);
while (--len >= 0) {
rv[len] = val;
}
return rv;
}
Fiddle #1: Array of objects using ng-model pointing to entry using $index and $parent.$index: http://jsfiddle.net/DFEkG/ -- All models update simultaneously
Fiddle #2: Array of objects using ng-model pointing to i instead of $index: http://jsfiddle.net/DFEkG/1/ -- All models update simultaneously
Fiddle #3: Array using ng-model pointing to entry using $index and $parent.$index: http://jsfiddle.net/DFEkG/2/ -- Weird behavior
Fiddle #4 Array using ng-model pointed to i instead of $index: http://jsfiddle.net/DFEkG/3/ -- Broken
Fiddle #5 Array using only $index and $parent.$index and array only in ng-repeat directive: http://jsfiddle.net/DFEkG/4/ -- WORKS! But not as object
Fiddle #6 Same as technique as 5, but with an object: http://jsfiddle.net/DFEkG/5/ -- Same as fiddles 1 and 2
The problem is in your filledArray function. It assigns the same exact object to each array item. The same object {input: '', correct: true} is referenced in all 60 instances.
So, to fix it you can simply make a new copy of it on each iteration:
function filledArray(len, val) {
var rv = new Array(len);
while (--len >= 0) {
rv[len] = angular.copy(val);
}
return rv;
}
Fiddle.

Resources