I'm making a browse page where users can see the top terms for my site's search facets (I'm using ElasticSearch/Tire.) I created an array of objects with a title and arguments in the format I need for search. I want to iterate through the array and display the title and then the results of my search for each facet. At first I tried using a for loop in the controller to iterate through facet_selections, but that didn't seem like the Angular way. So now I'm trying to use ng-repeat for the iteration, but I'm not sure how to pass the arguments from the view to the controller. I read through all the directives, and I don't see a good fit, which makes me think I might be on the wrong path all together.
Here is a simplified controller:
$scope.facet_selections=[{name:"Collection", value: "collection_title", term: "collectionTitle"}, {name:"Series", value: "series_title", term: "seriesTitle"}, {name:"Episode", value: "episode_title", term: "episodeTitle"},];
$scope.frequency=Frequency.query({facet: facet}).then(function(data) {
$scope.topterms=data.facets[term].terms;
})
And here's the html:
<div class="browse" ng-repeat="object in facet_selections" ng-init="var term={{object.term}}">
<h4> {{object.name}} </h4>
<ul>
<li ng-repeat="term in topterms"> {{term.term}} ({{term.count}})</li>
</ul>
</div>
The problem is that you can't bind the html to a promise. You have to wait for the promise to resolve then update the scope. So I would handle the initial loop in the controller, not with an ng-repeat.
$scope.facet_selections=[{name:"Collection", value: "collection_title", term: "collectionTitle"}, {name:"Series", value: "series_title", term: "seriesTitle"}, {name:"Episode", value: "episode_title", term: "episodeTitle"},];
for(var i; i < $scope.facet_selections.length; i++){
var selection = $scope.facet_selections[i];
Frequency.query({facet: selection.value}).then(function(data) {
$scope.facet_selections[i].results = data.facets[selection.term].terms;
$scope.$digest() // hook into the angular binding system
});
}
then
<div class="browse" ng-repeat="facet in facet_selections">
<h4> {{facet.name}} </h4>
<ul>
<li ng-repeat="term in facet.results"> {{term.term}} ({{term.count}})</li>
</ul>
</div>
Passing arguments from the view to the controller is extremely easy:
$scope.numbers = [0, 4, 5, 2];
$scope.getTimesTwo = function(num){
return num * 2;
};
then
<div ng-repeat="num in numbers">
{{num}} <span>{{getTimesTwo(num)}}</span>
</div>
will result in
<div ng-repeat="num in numbers">
0 <span>0</span>
</div>
<div ng-repeat="num in numbers">
4 <span>8</span>
</div>
<div ng-repeat="num in numbers">
5 <span>10</span>
</div>
<div ng-repeat="num in numbers">
2 <span>4</span>
</div>
Its just that that is not really your problem, its promise resolution hooking to the $digest cycle
Related
I am a beginner at angular. I am pretty certain I am doing this the completely incorrect way but because I finally have it "somewhat working" as it works on the second click I am stuck going in this direction and can't seem to figure out another way to do it.
The filter sorts on the second click because it is initialing as "undefined" before the first click and sets it based on that I believe.
In my html:
<div class="col-xs-12 col-sm-4 location-list" ng-repeat="key in careerlist.location">
<div class="locations" ng-click="careerlist.criteriaMatch()">{{key}}
</div>
</div>
<div class="col-xs-12">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 job-container" ng-repeat="job in careerlist.career | filter : searchText | filter: selectExperience | filter: careerlist.criteria.name">
<h2>
{{job.title}}
</h2>
<h3>
{{job.location}}
</h3>
<div class="job-description" ng-bind-html="job.description | limitHtml : 200">
</div>
<br><br>
<button>Read More</button>
</div>
<br><br>
</div>
</div>
In my controller:
cl.criteriaMatch = function( criteria ) {
jQuery(document).on('click', '.locations', function(){
cl.criteria = {};
console.log("just the element text " + jQuery(this).text());
cl.criteria.name = jQuery(this).text();
return function( criteria ) {
return criteria.name === criteria.name;
};
});
};
Use ng-click instead of jQuery#on('click'). Angular does not know that the filter should be updated.
Update
#Makoto points out that the click is bound twice. It very much looks like you should just remove the jQuery binding altogether. I would even go so far as suggesting removing jQuery from you project.
I'm using angular 1.4.8. I want to save filtered result from ng-repeat and use it to determine whether to enable a "load more" button at the bottom of the list. I have looked at this question:
AngularJS - how to get an ngRepeat filtered result reference
And many others, they suggest to use "as alias" from ng-repeat, here's what my code looks like:
<ul class="media-list tab-pane fade in active">
<li class="media">
<div ng-repeat-start="item in items | limitTo: totalDisplayed as filteredResult">
{{item}}
</div>
<div class="panel panel-default" ng-repeat-end>
</div>
<div>
{{filteredResult.length}}
</div>
</li>
</ul>
<button ng-click="loadMore()" ng-show="totalDisplayed <= filteredResult.length">
more
</button>
However, I found filteredResult.length is displayed fine right after ng-repeat, but the button is never shown. If I try to display filteredResult.length in where the button is, it will show null.
So is there a rule for "as alias" scope? I've seen plenty of examples work but mine doesn't, what am I missing here?
EDIT:
The accepted answer uses controllerAs which indeed will resolve the problem. However, charlietfl's comment using ng-init to save filteredResult to parent scope is also very neat and that's the solution I use in my code eventually.
Probably some of classes in your <ul class="media-list tab-pane fade in active"> or <li class="media"> is selector for a directive that would have its own scope. So you store filteredResult in e.g. tab-pane's scope and then try to have access out of it's scope in outside of ul tag.
Try to use Controller as instead of scope:
angular
.module('plunker', [])
.controller('MainCtrl', function() {
vm = this;
// make an array from 1 to 10
var arr = [];
while (arr.length < 10) {
arr.push(arr.length + 1)
};
vm.items = arr;
vm.totalDisplayed = 5;
vm.filteredResult = [];
});
<body ng-controller="MainCtrl as main">
{{main.items}}
<ul class="media-list tab-pane fade in active">
<li class="media">
<div ng-repeat-start="item in main.items | limitTo: main.totalDisplayed as filteredResult">
{{item}}
</div>
<div class="panel panel-default" ng-repeat-end>
</div>
<div>
filteredResult = {{main.filteredResult = filteredResult}}
</div>
</li>
</ul>
<button ng-click="loadMore()" ng-show="main.totalDisplayed <= main.filteredResult.length">
more
</button>
</body>
http://plnkr.co/edit/drA1gQ1qj0U9VCN4IIPP?p=preview
I want a live search: the results are queried from web api and updated as the user types.
The problem is that the list flickers and the "No results" text appears for a fraction of second, even if the list of results stays the same. I guess I need to remove and add items with special code to avoid this, calculating differences between arrays, etc.
Is there a simpler way to avoid this flicker at least, and probably to have possibility to animate the changes?
It looks like this now:
The html part is:
<div class="list-group">
<a ng-repeat="test in tests track by test.id | orderBy: '-id'" ng-href="#/test/{{test.id}}" class="list-group-item">
<h4 class="list-group-item-heading">{{test.name}}</h4>
{{test.description}}
</a>
</div>
<div ng-show="!tests.length" class="panel panel-danger">
<div class="panel-body">
No tests found.
</div>
<div class="panel-footer">Try a different search or clear the text to view new tests.</div>
</div>
And the controller:
testerControllers.controller('TestSearchListCtrl', ['$scope', 'TestSearch',
function($scope, TestSearch) {
$scope.tests = TestSearch.query();
$scope.$watch('search', function() {
$scope.tests = TestSearch.query({'q':$scope.search});
});
}]);
You should use ng-animate module to get the classes you need for smooth animation. For each ng-repeat item that's moved, added, or removed - angular will add specific classes. Then you can style those classes via CSS or JS so they don’t flicker.
An alternative way of doing what you require is to use the angular-ui bootstrap Typeahead component (check at the bottom of the post). It has a type-ahead-wait property in milliseconds and also a template url for customising it.
<div ng-app>
<div ng-controller="MyController">
<input type="search" ng-model="search" placeholder="Search...">
<button ng-click="fun()">search</button>
<ul>
<li ng-repeat="name in names">{{ name }}</li>
</ul>
<p>Tips: Try searching for <code>ann</code> or <code>lol</code>
</p>
</div>
</div>
function MyController($scope, $filter) {
$scope.names = [
'Lolita Dipietro',
'Annice Guernsey',
'Gerri Rall',
'Ginette Pinales',
'Lon Rondon',
'Jennine Marcos',
'Roxann Hooser',
'Brendon Loth',
'Ilda Bogdan',
'Jani Fan',
'Grace Soller',
'Everette Costantino',
'Andy Hume',
'Omar Davie',
'Jerrica Hillery',
'Charline Cogar',
'Melda Diorio',
'Rita Abbott',
'Setsuko Minger',
'Aretha Paige'];
$scope.fun = function () {
console.log($scope.search);
$scope.names = $filter('filter')($scope.names, $scope.search);
};
}
I have two columns both containing items from the same array. I want to achieve the masonry effect since the height of each .tile will be different.
<div class="col-md-6">
<div class="tile" ng-repeat="item in items| orderBy: 'id'" ng-if="$odd">
<button ng-click="alert($index)"></button>
</div>
</div>
<div class="col-md-6">
<div class="tile" ng-repeat="item in items| orderBy: 'id'" ng-if="$even">
<button ng-click="alert($index)"></button>
</div>
</div>
Will Angular do the odd-even alternation on the sorted array or the way elements are stored in the array?
Angular is going to alternate on the way the elements are stored in the array. If you want to organize the array I would use .sort() before assigning to Angular $scope.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
Based on the trying the code below, it seems that Angular does apply $odd and $even after sorting based on the orderBy filter. However, this would only work correctly in a single ng-repeat directive. I would modify your code to look like the following:
angular.module("myApp", [])
.controller("ItemController", function($scope) {
$scope.items = [{
id: 50
}, {
id: 1
}, {
id: 2
}, {
id: 5
}, {
id: 3
}];
$scope.alert = function(idx) {
console.log(idx);
};
});
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="myApp">
<div class="col-md-6" ng-controller="ItemController">
<div class="tile" ng-repeat="item in items| orderBy: 'id'">
<button class="btn btn-danger" ng-if="$odd" ng-click="alert($index)">{{item.id}}</button>
<button class="btn btn-success" ng-if="$even" ng-click="alert($index)">{{item.id}}</button>
</div>
</div>
</body>
In other words, you should apply the ng-if directive to the element inside your ng-repeat directive. Depending on what exactly you want to alternate, it might be simpler to use ng-class or another directive that allows you to specify conditional attributes.
The Angular orderBy filter returns a copy of the source array. The ng-repeat is applied to the copy.
Your example case will work, but it will create twice as many $watch elements, and you might experience odd flickering of the data shifting back and forth between columns if the number of rows changes. Also, because arrays are 0 based, your rows will be backwards.
I have a fairly large object that needs to be iterated over in nested ng-repeat loops
A simplified version looks like this:
{{totalEvents = 0}}
<div ng-repeat="(mainIndex, eventgroup) in EventsListings">
<ul>
<li ng-repeat="event in eventgroup.events" ng-click="Current(totalEvents)">
<div class="event-item-container" ng-show="eventDetailsView[totalEvents]">
{{totalEvents = totalEvents + 1}}
</div>
</li>
</ul>
</div>
{{totalEvents = 0}
How can I keep track of totalEvents counter value.. How can I get a total number of iterations across nested loops IN the template?
you can reach many value just using $index property of ng-repeat...
HTML
<div ng-repeat="eventgroup in EventsListings" ng-init="outerIndex = $index">
<ul>
<li ng-repeat="event in eventgroup.events" ng-click="Current(totalEvents)">
<div class="event-item-container">
{{event.name}} can have unique value like
<br/>(outerIndex) * (eventgroup.events.length) + innerIndex
<br/>Total Events = {{outerIndex * eventgroup.events.length + $index}}
<br/>Inner Events = {{$index}}
</div>
</li>
</ul>
</div>
here is working PLUNKER
UPDATE
After some times and some comments I realized that my code is not calculating total iterations correctly, so I made some changes to fix it.
First mistake I made somehow I thought event numbers will be equals for every set, second one if there are more than 2 sets again it fails.
So for keeping track of total iterations I set an array which is called totalIterations in code. In this array I set total number events we already iterate so far,
For example at the finish of first set it will be length of first event group and for second it will be first and second group, and so on... For achieving this I used ng-repeat-end directive here is the final code
<div ng-repeat="eventgroup in EventsListings" ng-init="outerIndex = $index">
<ul>
<li ng-repeat="event in eventgroup.events">
<div class="event-item-container">
{{event.name}} can have unique value like
<br/>Total Events Count = {{totalIterations[outerIndex- 1] + $index}}
<br/>Innder Events = {{$index}}
</div>
<button class="btn btn-success" ng-click="Current({{totalIterations[outerIndex- 1] + $index}})">Current Event</button>
</li>
<span ng-repeat-end ng-init="totalIterations[outerIndex] = totalIterations[outerIndex - 1] + eventgroup.events.length"></span>
</ul>
</div>
and here is latest PLUNKER
I want to suggest different way to approach this, I think it's pretty cool and easy :
In the template :
<ul>
<div ng-repeat="group in obj.objectGroups">
<li>{{group.name}}</li>
<li ng-repeat="item in group.items" ng-init="number = countInit()">
Total = {{number + 1}}
</li>
</div>
</ul>
In the controller :
$scope.totalCount = 0;
$scope.countInit = function() {
return $scope.totalCount++;
}
If you really want the template to drive this calculation, you could keep a counter in the Controller and create a function that increments that counter. Then call that function from the template.
Honestly, though, it seems very strange to put this sort of logic in the view. It would make much more sense just to do a recursive count in pure Javascript in the Controller.