Toggle classes in a list with Angular - angularjs

I'd like to toggle a class when an element is clicked using AngularJS. I need the clicked element to received the class, and any other items in the list to loose the class. I've researched a number of supposed solutions for this on SO, however in implementing them, they don't work appropriately and I don't understand why they would even.
The general solution proposed is to set a variable to the index of the item in the ng-repeat list. Then use ng-class to add the class. JSFiddle here.
<div ng-app>
<p ng-repeat="item in ['a', 'b', 'c']"
ng-click="selectedIndex = $index"
ng-class="{selected: $index === selectedIndex}"
>{{item}}</p>
</div>
The problem is that the 'selected' class is never removed from the previous elements. So, clicking an element adds the class to the element as expected, but clicking another element doesn't remove the class from previously clicked elements. I would guess because Angular is not re-rendering the entire list on each click and thus the old clicked elements don't change. That begs my question though, why is this such an overwhelmingly proposed solution? Am I just implementing something wrong? Thanks.

That's because ng-repeat creates its own child scope, so your other elements each have their own instance of selectedIndex - create a function so selectedIndex is seen by all repeated elements:
Controller:
$scope.setSelected = function(index) {
$scope.selectedIndex = index;
}
<div ng-app>
<p ng-repeat="item in ['a', 'b', 'c']"
ng-click="setSelected($index)"
ng-class="{selected: $index === selectedIndex}"
>{{item}}</p>
</div>

Related

Angular.js ng-repeat unique identifier

I am using ng-repeat on a <tr> tag to populate the <td> tags with data pulled from mysql and converted into Json. This works just fine. However, one of the <td> tags that I'm using contains a button.
What I would like to do, is have each of these buttons identified somehow in the DOM, so that I can target then with specific requests.
Example: Page loads, ng-repeat repeats a button 4 times. Each of these buttons would have an ng-click attached to it. I want each of them to open and filter different information in a json file.
Am I correct in assuming that ng-repeat would simply open the same item for each button, and how would I go about making them seperate? thanks.
You can do something like this on the front-end:
<button ng-repeat="item in items track by $index" ng-click="someFunction($index)" >Something happens</button>
Then in your controller:
$scope.someFunction = function (index) {
if (index === 1):
// etc.
else...
// Or use switch, whichever works for you.
You could create the specific function on each item in the array.
<button ng-repeat="button in buttons" ng-click="button.functionName()">{{button.name}}</function>
There's $index for that. It's a very good habit to take for any of your ng-repeat. Also don't forget bind-once if your buttons UI isn't subject to modifications once the DOM has loaded.
<ul>
<li ng-repeat="page in pages">
<a ng-class="{ testClass: $index == pageNumber }" ng-click="setPageNumber($index)">{{ page }} - index is : {{$index}}</a>
</li>
</ul>
http://jsfiddle.net/bonatoc/4z1t4gsm/3/
Also you could do (using bind-once):
<button
ng-repeat="button in ::buttons track by $index"
id="button_{{$index}}"
class="{{button['css_class']}}"
...given your buttons were a JSON object as well (beware, ng-repeat likes arrays, not objects. Arrays of objects are fine):
$scope.buttons = [
0: {
'css_class': someClass,
'functionToTrigger': function(...
// ...

Programmatically Open Bootstrap UI Accordion Generated by Nested Ng-Repeat when Filtered Array is Not Empty

I have a Bootstrap-UI accordion group that generates individual accordion menus using ng-repeat. The content under each accordion is also generated using a nested ng-repeat.
<accordion close-others="false">
<span data-ng-repeat="category in categories">
<accordion-group is-open="filterText.length > 0">
<accordion-heading>{{category}}: {{reportList.length}} Items </accordion-heading>
<div>
<ul>
<li data-ng-repeat="report in reportList = (getReports($parent.$index) | filter: filterText)">{{report}}</li>
</ul>
</div>
</accordion-group>
</span>
</accordion>
The content generated by the second ng-repeat needs to be searchable. When the search is executed, accordions that contain matching values should open and display the matching values. I know that the outside ng-repeat sees the filtered array and its length because i can display the length value in the view (i.e. {{reportList.length}} updates when the filter executes).
The problem comes when i try to put the reportList.length value in the is-open attribute of an <accordion-group>. Using is-open="reportList.length" seems to pass the literal into the directive. In desperation, I tried using is-open="{{reportList.length}}" but that throws the expected syntax error.
Here's a plunker that shows what i have working as well commented out lines that show the way i think it should work (lines 22 & 23). Any suggestions are more than welcome.
Your binding is-open inside of an ng-repeat which creates a child scope for each item (category). You need to bind to $parent.filterText.length as filterText is not a category property.
What you bind the is-open to, Angular needs to be able to assign to it, not just evaluate it. It can evaluate an expression like "foo == 5" but it cannot assign to it.
What I do is create a variable and bind to that, then the accordion can change it, and I can change it, and everybody's happy.
$scope.accordionSettings = {
IsOpen: {
Section1: true,
Section2: false
}};
In the markup:
<div uib-accordion-group is-open="accordionSettings.IsOpen.Section1">
... more markup ...
<div uib-accordion-group is-open="accordionSettings.IsOpen.Section2">

Pagination repeater has an active class that i must move when pagination button clicked

Take the following pagination html and repeater:
<ul id="ProductListPagination" class="pagination">
<li class="disabled"><span aria-hidden="true">«</span><span class="sr-only">Previous</span></li>
<li ng-repeat="n in PageCount" ng-class="{active: n==1}"><a ng-click="Paginate( n )" href="#"><% n %> <span class="sr-only">(current)</span></a></li>
</ul>
How do I go about moving the active class on the repeater when one of the pagination buttons are pressed... Is there a built in angular way?...
context:
If there is no built in way in angular, how do I pass the dom element through to the Paginate( n ) function?
I have the receiving function:
$scope.Paginate = function( obj, page ){
// Remove currently active button's active class
$( "#ProductListPagination li.active" ).each( function() {
$( this ).removeClass( "active" );
} );
// Add to element just clicked on
$( obj ).parent().addClass( "active" );
...
}
And the html to go with that, you'll see I tried passing in this.
<li style="cursor:pointer" ng-repeat="n in PageCount" ng-class="{active: n==1}"><a ng-click="Paginate( this, n )" href="#"><% n %> <span class="sr-only">(current)</span></a></li>
But that doesn't work as this, is not a dom element.
Directives
This could be done with Directives, these allow you to define custom markup ie. an element, an attribute name, it even can hook onto class names. Then from that you can attach all sorts of stuff.
https://egghead.io/lessons/angularjs-first-directive
So you could make a "Pagination" directive.
and that would then be used something like:
<my-pagination pages="arrayInScope">
You can then provide an external template or even a string of markup (eww) for what needs to be either IN this element or to replace this element.
Another Solution
But this is a quick way and I guess it doesn't really need any more over complicating anyway.
Example on CodePen
From the markup I am calling the paginate function parsing ng-repeat's provided $index. In the paginate function in the js i then set it.
As angular digests this: ng-class="{ active : page == current }" will then re-evaluate.
But if you need access to the element for some reason then use directives. jQuery is best avoided when using Angular, if it is just a class change or a visibility toggle etc. then its best to let Angular do it for ya'
Hope that helps.

Getting the jQuery object that has been clicked on in angular via $event

I have a list like this. It is pre rendered and so cannot make use of anything attached to ng-repeat.
<li ng-class="{ 'active': 1 == selectedIndex }">
Item 1
</li>
I want to be able to toggle the class of the <li> when the a is clicked.
Looking at some other answers to similar questions on here, it seems as though there is a variable associated with ng-repeat which means you can use an $index variable to achieve this. As this list is pre-rendered this is not available and so I guess I have to do it the jQuery way.
I see that I have access to an $event object but event.target only gives me the DOM element, I would like to be able to convert it into a jQuery object. Is this possible?
You have to think about this in a different way than you would normally. You can't modify the DOM within angular like you do with plain old jquery. Here is what you should do:
<li ng-class="{ active: selectedItem == item }" ng-repeat="item in list">
{{ item.name }}
</li>
Then in your controller:
$scope.selectedItem = null; // if this is loaded from a service then you can set it after it loads.
$scope.itemSelected = function( item ) {
$scope.selectedItem = item;
}
No need to play around with indexes, jquery, or one off code.
Charlie

Angularjs: custom grid component, dynamically add <br> elements

Ok, I have made a really simple grid component. I fetch a column count attribute and add a <br> tag after the end of a row. I do that within the link function.
Her is the directive: http://pastebin.com/U4ckuKJw
grid.html just looks like this: <div class="grid" data-ng-transclude=""></div>
In my first example I have 7 <div> tags inside the grid component and want to have 3 columns. So after every third <div> I want a <br> to be added.
It looks like this and is working:
<div data-grid="" data-cols="5">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
</div>
This one is not working: http://pastebin.com/wtcgM2Hv
I think it is because of the directive ng-repeat and that the content of the grid component isn't rendered at the time the link function ist executed.
Any thoughts on how to solve this problem or how to optimise the component?
An easier approach may be to solve the problem with standard angular directives and css. For instance, if you float your cells and clear: left on those cells that start a new row, you can use ng-repeat and ng-class to accomplish this.
I created an example at: http://jsbin.com/idukaz/1
The html looks like this. Note I'm using ng-class to apply the class that formats the cell that needs to start a new row depending on the result of calling the custom columnEnd function. columnBreak is a scope variable for the number of columns you want. $index is a variable generated by ng-repeat:
<div class="table">
<div class="label"
ng-class="{'new-row': startNewRow($index, columnBreak) }"
ng-repeat="item in items">{{ item.name }} ({{ $index + 1 }})</div>
</div>
In your controller:
app.controller('Controller', ['$scope', function (scope) {
// list of grid data
scope.items = [];
// controls the number of columns
scope.columnBreak = 5;
// calculates if current cell is start of new row
scope.startNewRow = function (index, count) {
return ((index) % count) === 0;
};
}]);
In your css if you float your cells and clear left, you'll get your columns to dynamically
reformat when you change the columnBreak value.
.label {
float: left;
}
.new-row {
clear: left;
}
Ok, now I found the solution:
use angular directive to change classes of ng-repeat elements
I mixed it with Marks idea and manipulate the css classes now.
Thanks

Resources