AngularJs get count of element by class name - angularjs

How can I count element by class name in angularJs?
I have tried with:
$scope.numItems = function() {
$window.document.getElementsByClassName("yellow").length;
};
Plunkr: http://plnkr.co/edit/ndCqpZaALfOEiYieepcn?p=preview

You have defined your function correctly, but made a mistake in showing its results: it should have been...
<p>{{numItems()}}</p>
... instead of plain {{numItems}}. You want to display the return value of the function, and not the function itself (that's meaningless), that's why you should follow the standard JS syntax for a function invocation.
Note that you can send arguments into this expression too: for example, I've rewritten that method like this:
$scope.numItems = function(className) {
return $window.document.getElementsByClassName(className).length;
};
... and then made three different counters in the template:
<p>Yellow: {{numItems('yellow')}}</p>
<p>Green: {{numItems('green')}}</p>
<p>Red: {{numItems('red')}}</p>
Plunker Demo.
But here's the real problem: numItems() result, used in one View, is based on DOM traversal - in other words, on another View. Not only that goes against Angular philosophy in general, it tends to break. In fact, it DOES break since this commit, as old as 1.3.0:
Now, even when the ngAnimate module is not used, if $rootScope is in
the midst of a digest, class manipulation is deferred. This helps
reduce jank in browsers such as IE11.
See, changes in classes are applied after digest - and that's after numItems() is evaluated, hence the delay in demo mentioned by #Thirumalaimurugan.
A quick-and-dirty solution is using another attribute for selector in numItems (in this plunker, it's data-color). But I would strongly advise you against it. The proper approach would be adding the data rendered by numItems() -using component into the model. For example:
app.js
// ...
var colorScheme = {
'toggle': {true: 'yellow', false: 'red'},
'toggle2': {true: 'green', false: 'red'},
'toggle3': {true: 'green', false: 'red'},
'toggle4': {true: 'red', false: 'green'}
};
$scope.getColor = function getColor(param) {
return colorScheme[param][$scope[param]];
};
$scope.countColor = function(color) {
return Object.keys(colorScheme).filter(function(key) {
return colorScheme[key][$scope[key]] === color;
}).length;
};
index.html
<p ng-class="getColor('toggle')">{{name}}</p>
<!-- ... -->
<p ng-repeat="color in ['Yellow', 'Green', 'Red']"
ng-bind="color + ' ' + countColor(color.toLowerCase())">
Demo.

Related

AngularJS ng-repeat update does not apply when object keys stay the same?

I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.

angularjs change ng-repeat variable and call function

I am trying to toggle a class when clicking on the Save button but also call a function. I am using coffeescript. The function gets called but the variable never gets set to false.
div(ng-class="{'someclass':setListFocus}, ng-repeat="item in items")
a(ng-click="setListFocus=false;someFunction();")
span(class="gs-desktop") Save
// Delete user data.
a(ng-click="setListFocus=true")
span Edit
I assume that your small example is missing all kinds of data.
I would recommend either using controllerAs, for example:
<div ng-controller="myController as vm">
and then changing your bindings to vm.setListFocus.
Or another option would be to change your controller code like so:
function myController($scope) {
$scope.list = { focus: false };
}
And change your binding to list.focus instead. This will solve your scope inheritance problems.
You could begin someFunction() with toggling the value of setListFocus
$scope.someFunction = function(){
$scope.setListFocus = !$scope.setListFocus;
// ... rest of someFunction()
}
And then the angular template would be just this:
a(ng-click="someFunction()")
Another thing you could do is set up a toggle ListFocus function, that can take an optional callback parameter. This means you can toggle the listfocus between true or false, and optionally execute another function. I'm not going to continue answering in coffeescript because I'm not that familiar, and not everyone is.
$scope.toggleListFocus = function(andThen = false){
$scope.listFocus = !$scope.listFocus;
if(andThen==false) andThen();
}
//usage..
//toggle $scope.listFocus
enter code here
a(ng-click="toggleListFocus()")
//toggle then someFunction()
a(ng-click="toggleListFocus(someFunction())")

Difficulty adding multiple classes wit ng-class

I'm trying to add two classes to this div using ng-class, but even though checkForActive is returning true, it's being ignored and only class_{{$index}} is getting added.
If I remove class_{{$index}} altogether, active is added correctly.
Is there an obvious mistake in my syntax here?
<div "ng-class="{active: checkForActive, disabled: checkForDisable, class_{{$index}}} "></div>
You could just provide true value to the key class_{{$index}} just to that property gets added as a class name to the class list of the element. It is just the way you do active: checkForActive.
i.e
{active: checkForActive, disabled: checkForDisable, class_{{$index}} :true}
But i believe there could be some undesired behavior due the usage of interpolation ({{) within the ng-class directive (Atleast used to happen with older versions). So you could as well use an array.
ng-class="[checkForActive && 'active' , checkForDisable && 'disabled', 'class_' + $index]"
The above method will add a class name false if active or disabled is false, which should be harmless.
Or pass index to a controller function say getStatus($index) and return the object from there and use it in the ng-Class directive.
$scope.getClass = function(){
var obj = {active: $scope.checkForActive, disabled: $scope.checkForDisable};
obj['class_' + this.$index] = true;
return obj;
}
and
ng-class="getClass()"
#Okazari pointed out that it indeed works by mixing class with ng-class so you could also do:
class="class_{{$index}}" ng-class="{active: checkForActive, disabled: checkForDisable}"

Using ng-init to call controller functions and passing vars between controllers

The below method works...however, it only does so with $timeout added to the tabList() function. The ng-init is executing before the DOM renders thus the document.getElementById('')'s is coming back as undefined. I must force a delayed timer of 1 to 2 seconds until the DOM loads before appending the elements. This is not optimal but it does work. I am looking for another method that is cleaner and not dependent on delayed execution.
angular.module('starter.controllers', [])
.constant('constants', {
tabColors: {
curID:0,
},
})
.controller('TabsCtrl', function($scope,Tabs,constants) {
$scope.constants = constants;
$scope.tabList = function() {
var tID = $scope.constants.tabColors ;
console.log(tID.curID) ;
if (tID.curID) {
$timeout(function() {
document.getElementById('bike_tabItem_'+tID.curID).style.color = 'green' ;
document.getElementById('bike_tabItem_'+tID.curID).style.color = 'black' ;
},1000) ;
}
}
})
.controller('TabDetailCtrl', function($state,$scope,$stateParams,Tabs,constants) {
$scope.constants = constants; //make it available constants on html
$scope.itemSelect = function(thisID) {
$scope.constants.tabColors.oldID = $scope.constants.tabColors.curID ;
delete $scope.constants.tabColors['tabID_'+$scope.constants.tabColors.curID] ;
$scope.constants.tabColors.curID = thisID ;
$scope.constants.tabColors['tabID_'+thisID] = 'green' ;
}
})
// In HTML on Tab.html :
<ion-item cache-view="false" id="tab_tabItem_{{tab.tabID}}" ng-init="tabList()">
// In HTML on Tab-Detail.html
<button id="tab_button" class="button button-small button-outline button-positive" ng-click="itemSelect({{tab.tabID}});">
Select this item
</button>
On a side note, another way to call tabList() is like:
ng-init="tabList('{{tab.tabID}}')"
This gives you a way of passing values through the ng-init which, unlike my above call, gives you better control without having to define globals. Though you would still need a global for the above to track which element was turned green so you could then set it back to black before setting the new element green.
As said in the AngularJS documentation you should avoid the use of ng-init.
The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.
To passes variables between controllers you can use a provider.

AngularJS animate dynamic margin

I have an element that appears when the user clicks a button elsewhere on the screen. The element appears to come out of the top of the screen. The element by default needs to be tucked out of view above the screen, so I will have a margin-top style that is based on the height of the element (and will be a negative value). This cannot be hardcoded in css because the element height may vary. When I click the button, I want the element margin-top to change to 0 and I want a transition animation.
The sample shown on angularJS documentation is for adding a removing a class. This would work fine if I knew the values to be set and could code them in CSS, however I cannot. What is the correct way to solve this?
The code below works for displaying and hiding my element using a margin but there is no animation. How do I trigger an animation here when the margin changes?
https://docs.angularjs.org/guide/animations
Quote Total: {{salesPriceTotal + taxesTotal - tradeInsTotal | currency}}
<div class="totals" ng-style="setTopMargin()">
// totals stuff here.
</div>
$scope.setTopMargin = function() {
return {
marginTop: $scope.marginTop
}
};
$scope.$watch('showTotals', function() {
var margin = $scope.showTotals ? 10 : -160 + $scope.modelTotals.length * -200;
$scope.marginTop = margin.toString() + 'px';
});
I added the following code per a suggested solution, but this code is never hit.
myApp.animation('.totals', function () {
return {
move: function (element, done) {
element.css('opacity', 0);
jQuery(element).animate({
opacity: 1
}, done);
// optional onDone or onCancel callback
// function to handle any post-animation
// cleanup operations
return function (isCancelled) {
if (isCancelled) {
jQuery(element).stop();
}
}
},
}
});
As the documentation explains: "The same approach to animation can be used using JavaScript code (jQuery is used within to perform animations)".
So you basically needs to use animate() from jQuery to do what you want.

Resources