Problems with `track by $index` with Angular UI Carousel - angularjs

$index in track by index does not start at zero when pagination is used
I have created a carousel using angular ui bootsrap.
Since am loading so many images(over 1,000), I used a filter to display 500 pictures in the pagination.
.filter('pages', function () {
return function (input, currentPage, pageSize) {
if (angular.isArray(input)) {
var start = (currentPage - 1) * pageSize;
var end = currentPage * pageSize;
return input.slice(start, end);
}
};
})
The controller:
self.currentPage = 1;
self.itemsPerPage = 500;
self.maxSize = 10;
//imagesUrls is response.data from http call
angular.forEach(imagesUrls, function (parent) {
var date = uibDateParser.parse(parent.fileCreationTime, 'M/d/yyyy
hh:mm:ss a');
// fill our slide arrays with urls
// model update here
self.slides.push({
image: parent.fileName,
time: date,
id: parent.id
});
});
self.totalItems = self.slides.length;
And I use it like this:
<div uib-carousel
active="$ctrl.active"
interval="$ctrl.myInterval"
no-wrap="$ctrl.noWrapSlides"
no-pause="true">
<div uib-slide ng-repeat="slide in $ctrl.slides | pages:
$ctrl.currentPage : $ctrl.itemsPerPage track by $index"
index="$index">
<img id="carousel-img" ng-src="{{slide.image}}">
<div class="carousel-caption">
<p>Index {{$index}}</p>
</div>
</div>
</div>
<div class="row">
<ul uib-pagination
total-items="$ctrl.totalItems"
items-per-page="$ctrl.itemsPerPage"
ng-model="$ctrl.currentPage"
max-size="$ctrl.maxSize"
boundary-link-numbers="true"
force-ellipses="true"
class="pagination-sm">
</ul>
</div>
This works as expected.
When the carousel is first loaded, the index is 0. When it moves to the next slide the index is 1, when you move to the next slide the index is 2.
When you display the slide.id of the current image it is also 2.
The problem:
However, when you click the second pagination link, the index does not go back to zero, its starts at the last index the slide was in the carousel.
So now the index is 2 and slide id of the current image is 502.
If you slide till index 20, and you click the pagination link, the index is still at 20. When you display the slide id of the current image it becomes 520.
Is there a way to make the index start at 0 again so the slide.id is 500 and not 502 or 520?
I hope my question is clear.

Avoid track by $index when there is a unique property identifier to work with. When working with objects that are all unique (as is this case), it is better to let ng-repeat to use its own tracking instead of overriding with track by $index.
<div uib-slide ng-repeat="slide in $ctrl.slides | pages:
$ctrl.currentPage : $ctrl.itemsPerPage track by ̶$̶i̶n̶d̶e̶x̶ slide.id"
index="$index">
<img id="{{slide.id}}" ng-src="{{slide.image}}">
<div class="carousel-caption">
<p>Index {{ ̶$̶i̶n̶d̶e̶x̶ slide.id}}</p>
</div>
</div>
From the Docs:
If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
— AngularJS ng-repeat Directive API Reference - Tracking
The DEMO
angular.module("app",['ngAnimate', 'ngSanitize', 'ui.bootstrap'])
.controller("ctrl", function(uibDateParser) {
var self = this;
self.currentPage = 1;
self.itemsPerPage = 10;
self.maxSize = 10;
var url = '//unsplash.it/200/100';
var imagesUrls = [];
for (let i=0; i<40; i++) {
var slide = {
fileName: url+"?image="+(1000+i),
id: 'id'+(0+i+100),
fileCreationTime: new Date()
}
imagesUrls.push(slide);
}
self.slides = [];
//imagesUrls is response.data from http call
angular.forEach(imagesUrls, function (parent) {
var date = uibDateParser.parse(parent.fileCreationTime,
'M/d/yyyy hh:mm:ss a');
// fill our slide arrays with urls
// model update here
self.slides.push({
image: parent.fileName,
time: date,
id: parent.id
});
});
//console.log(self.slides);
self.totalItems = self.slides.length;
})
.filter('pages', function () {
return function (input, currentPage, pageSize) {
if (angular.isArray(input)) {
var start = (currentPage - 1) * pageSize;
var end = currentPage * pageSize;
return input.slice(start, end);
}
};
})
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/angular-animate/angular-animate.js"></script>
<script src="//unpkg.com/angular-sanitize/angular-sanitize.js"></script>
<script src="//unpkg.com/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js"></script>
<link href="//unpkg.com/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<body ng-app="app" ng-controller="ctrl as $ctrl">
<div class="container" uib-carousel
active="$ctrl.active"
interval="$ctrl.myInterval"
no-wrap="$ctrl.noWrapSlides"
no-pause="true">
<div uib-slide ng-repeat="slide in $ctrl.slides | pages:
$ctrl.currentPage : $ctrl.itemsPerPage track by slide.id"
index="$index">
<img id="{{slide.id}}" ng-src="{{slide.image}}">
<div class="carousel-caption">
<p>Index {{slide.id}}</p>
</div>
</div>
</div>
<div class="row container">
<ul uib-pagination
total-items="$ctrl.totalItems"
items-per-page="$ctrl.itemsPerPage"
ng-model="$ctrl.currentPage"
max-size="$ctrl.maxSize"
boundary-link-numbers="true"
force-ellipses="true"
class="pagination-sm">
</ul>
</div>
</body>

Related

Filter the object in the array by dynamic filter - AngularJS

Right now my data structure is like
product = [{att1:'2',att2:'red',att3:'gold'},
{att1:'1',att2:'blue',att3:'wood'},
{att1:'2',att2:'green',att3:'plastic'},
{att1:'1',att2:'red',att3:'plastic'}]
And I have a filter on the web page, it has three parts: att1, att2, att3. The user doesn't have to choose options for every part.
For filter att1 it has 2 options: "1" and "2".
Filter att2 it has 2 options: "red" "blue" and "green"
Filter att3 it has 3 options: "gold", "wood" and "plastic".
I can get the options that are selected. For example:
{att1:['2'],att3:['gold','plastic']} or {att1:['1']}
My question is, how do I use product.filter to filter the product data?
Thanks!
You can use a custom filter function which is easy to use, I used att1 but you can expand it to all fields:
var app = angular.module("MyApp", []);
app.controller("MyCtrl", function($scope) {
$scope.products = [{att1:'2',att2:'red',att3:'gold'},
{att1:'1',att2:'blue',att3:'wood'},
{att1:'2',att2:'green',att3:'plastic'},
{att1:'1',att2:'red',att3:'plastic'}];
$scope.filterFunction = function(element) {
return element.att1.match(/^Ma/) ? true : false;
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="MyApp">
<div ng-controller="MyCtrl">
<form class="form-inline">
<input ng-model="query" type="text"
placeholder="Filter by" autofocus>
</form>
<ul ng-repeat="product in products | filter:query">
<li>{{product}}</li>
</ul>
<ul ng-repeat="product in products | filter:filterFunction">
<li>{{product}}</li>
</ul>
</div>
</div>
I banged out the particular logic for this one, using the three nested loops isn't super great but it does do the job. I'm sure you can optimize this further by using some maps or something but I just went with the get it done by brute force approach :)
angular.module('myApp',[])
.service('ProductService', function(){
return {
products:[
{att1:'2',att2:'red',att3:'gold'},
{att1:'1',att2:'blue',att3:'wood'},
{att1:'2',att2:'green',att3:'plastic'},
{att1:'1',att2:'red',att3:'plastic'}
]
}
})
.controller('TestCtrl', function(ProductService){
this.ProductService = ProductService;
this.filterObject1 = {att1:['2'],att3:['gold','plastic']};
this.filterObject2 = {att1:['1']};
})
.filter('productFilter', function(){
return function(input,filterObj){
if(!filterObj){
return input;
}
var newArray = [];
var filterKeys = Object.keys(filterObj);
for(var i=0;i<input.length;i++){
var curElement = input[i];
innerLoops:
for(var j=0;j<filterKeys.length;j++){
var curKey= filterKeys[j];
for(var k=0;k<filterObj[curKey].length;k++){
var curFilterValue = filterObj[curKey][k];
if(curElement[curKey].match(curFilterValue)){
//We found a match keep this element and move on to checking the next one by breaking out of the inner loops that are checking particular keys/values
newArray.push(curElement);
break innerLoops;
}
}
}
}
return newArray;
};
})
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.js"></script>
<div ng-app="myApp" ng-controller="TestCtrl as ctrl">
Unfiltered
<div ng-repeat="product in ctrl.ProductService.products|productFilter">
{{product}}
</div>
<hr/>
<strong>Filtered with: {{ctrl.filterObject1}}</strong>
<div ng-repeat="product in ctrl.ProductService.products|productFilter:ctrl.filterObject1">
{{product}}
</div>
<hr/>
<strong>Filtered with {{ctrl.filterObject2}}</strong>
<div ng-repeat="product in ctrl.ProductService.products|productFilter:ctrl.filterObject2">
{{product}}
</div>
</div>

using ng-repeat with api json in a for-loop

I am developing a mini app using angularjs that would grab data from a news api. I have succeeded in getting an array of 10(just the amount i want) articles from the array of 10 news sources( all provided by the api) using a for-loop(below). The problem is that the ng-repeat in the view only displays the last iteration of the loop. How can i get it to display all the iterations?
here is the controller:
angular.module('newsApp',[])
.controller('newsController',['$scope','$http', function($scope,$http){
var index = 0;
var sortby = ['top','latest','popular'];
$http.get('https://newsapi.org/v1/sources?language=en').then(function(response){
var sourceIdArray = [];
var articlesArray = [];
for (var i = 0; i < 9; i++) {
$scope.getId = response.data.sources[i].id;
sourceIdArray.push($scope.getId);
$http.get(' https://newsapi.org/v1/articles?source=' + sourceIdArray[i] + '&apiKey=53bec2b512724f58b92203f0f7e93dc1').then(function(response){
$scope.comits = response.data.articles
articlesArray.push($scope.comits);
});
}
})
}])
The loop gives me all the required articles but i don't know how to write the ng-repeat to display all the data instead of only the last iteration
and the html:
<div class="row rowdiv" ng-repeat="comit in comits" ng-if="$index % 2 == 0">
<div class="col-md-6">
<img ng-src="{{comits[$index].urlToImage}}" alt="">
<h3><a ng-href="{{comits[$index].url}}">{{comits[$index].title}}</a></h3>
<p>{{comits[$index].description}}</p>
<h5>{{result}} {{comits[$index].author}}</h5>
<h6 class="pull-right">{{comits[$index].publishedAt}}</h6>
</div>
<div class="col-md-6">
<img ng-src="{{comits[$index +1].urlToImage}}" alt="">
<h3><a ng-href="{{comits[$index +1].url}}">{{comits[$index +1].title}}</a></h3>
<p>{{comits[$index + 1].description}}</p>
<h5>{{result}} {{comits[$index + 1].author}}</h5>
<h6 class="pull-right">{{comits[$index +1].publishedAt}}</h6>
</div>
</div>
any help would be appreciated!
You are pushing the articles into the articlesArray with articlesArray.push($scope.comits);
You need to make articlesArray a member of $scope, then access it with ng-repeat. $scope.comits only contains the last article retrieved based on your code.
Or declare comits as $scope.comits = [] then change $scope.comits = response.data.articles to $scope.comits.push(response.data.articles)

How to reference divs by class by index in AngularJS?

I am new to AngularJS. This is my first Plunker.
I am trying to make it so that if a user clicks on a link in a section, the value of the link appears in the results. The other two results should be cleared of their values.
I can't find any documentation on how to reference specific items in a class by index. In jQuery, I would normally do something like...
// get this index of the thing that was clicked
$this = $(this),
Idx = $BUTTONS.BigButtons.index($this),
ThisButton.eq(Idx).doSomething();
<body ng-controller="MainController as main">
<div class="title" ng-repeat="i in [1,2,3]">
<p>This is section {{i}}</p>
<ul>
<li ng-repeat="j in [1,2,3]">
section {{i}} - link {{j}}</li>
</ul>
<div class="results" ng-bind="main.results"></div>
</div>
</body>
var app = angular.module('controllerAsDemo', []);
app.controller('MainController', function() {
this.results = "Lame default";
this.displayContent = function(firstIdx, secondIdx) {
this.results = "populate section "
+ firstIdx
+ " with the number "
+ secondIdx
+ " - other two results should be empty"
;
};
});
Here is the Demo
I think you should not be looking at classes to achieve what you want. Angular repeat and various other directives give you the ability to do that.
I have touched your code a little and it works now. See below.
var app = angular.module('controllerAsDemo', []);
app.controller('MainController', function($scope) {
$scope.name = 'Cool Clicking';
$scope.results = [];
$scope.displayContent = function(firstIdx, secondIdx) {
$scope.results = [];
$scope.results[firstIdx] = "populate results "
+ firstIdx
+ " with the number "
+ secondIdx
+ " - other two results should be empty"
;
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="controllerAsDemo" ng-controller="MainController as main">
<p>{{main.name}}!</p>
<!-- LOOP THROUGH A GALLERY THEME -->
<div class="title" ng-repeat="i in [1,2,3]">
<p>This is section {{i}}</p>
<ul>
<li ng-repeat="j in [1,2,3]">
section {{i}} - link {{j}}
</li>
</ul>
<div class="results" ng-bind="results[i]"></div>
</div>
</body>
That said, if you want to use jQuery in Angular then you simply can.
I will suggest you read about Angular Dom Manipulation here
Another approach can be seen in this plunkr where each item in the gallery has his result.

post.length stays 0 due to loop - AngularJS

I'm trying to add pagination but I can't seem to figure out this last part.
Everything is setup, though my pagination isn't recording the amount of posts that are linked with the user.
Seeing that I'm doing a forEach and if loop and pushing the retrieved items into a empty collection, my 'posts.length' is returning 0.
Hence the pagination only showing page 1/1 and not 1/2 (for example).
Here is my full code:
profileCtrl.js
Here is the $http.get - I'm trying to get all the posts that the logged in user made doing this loop:
app.controller('profileCtrl', function($scope, auth, $http, $log) {
$scope.auth = auth;
$scope.date = auth.profile.created_at;
$scope.pageSize = 5;
$scope.posts= [];
$http.get('URL')
.then(function(result) {
angular.forEach(result.data, function(data, key) {
if(data.userId === auth.profile.user_id) {
$scope.posts.push(data);
}
});
});
});
profile.html
As you can see, I'm trying to get the length of post in posts using total-items="posts.length":
<div class="col-md-8 no-padding-right">
<div class="panel panel-primary">
<div class="list-group-item active text-center">
<h4 class="no-margin-top no-margin-bottom">Recent Activity</h4>
</div>
<a href="#" class="list-group-item" ng-repeat="post in posts| startFrom: (currentPage - 1) * pageSize | limitTo: pageSize | orderBy :'created_at':true">
<div class="row">
<div class="col-md-4">
<div class="thumbnail no-border no-margin-bottom">
<img src="https://placehold.it/150x150" alt="bird" width="150" height="150"/>
</div>
</div>
<div class="col-md-8">
<h4 class="no-margin-top no-margin-bottom"><strong>{{post.birdname}}</strong></
</div>
</div>
</a>
<uib-pagination total-items="posts.length" ng-model="currentPage" max-size="pageSize" boundary-link-numbers="true"></uib-pagination>
</div>
</div>
app.js
I also added a filter in app.js:
app.filter('startFrom', function() {
return function(data, start) {
return data.slice(start);
}
});
When I console.log(posts.length); I keep getting 0 and I'm guessing it's because of the $scope.posts = []; declared on top (profileCtrl.js).
Edit:
After doing a bit of debugging with console.log, I do get the value given when doing this:
$http.get('url')
.then(function(result) {
angular.forEach(result.data, function(data, key) {
if(data.userId === auth.profile.user_id) {
$scope.posts.push(data);
}
});
console.log($scope.posts.length);
});
How should I fix this?
If you're waiting for data to be returned before loading the collection (with pagination) either add a ng-if="posts.length" to the container, or initialise $scope.posts as being null and add ng-if="posts" if you want the list to show when the API returns 0 results. This will prevent Bootstrap's pagination directive being parsed until the data it needs is available.
Edit: After debugging, the following plunkr contains a working implementation: http://plnkr.co/edit/VQjNVK6gRKsCqxVb54nR?p=preview

How to display custom layout with ng-repeat

Edit: My original question was not good enough, so edited it now.
Hi Im new to angular and im trying to display a custom Layout
for my data list with ng-repeat. I cant use ng-class, as I would like to display diffent model vaules too.
I implemented a function in my controller, that calculates true or false according to my desired design. Then im trying to use ng-if to display my desired HTML with the data. The way I implemeted seems a bit awkward, especially if the layout gets more complicated, is there a better way to achieve this behaviour?
Here a sample: http://plnkr.co/edit/puE7OE?p=info
Controller:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.eventModels=[{name:'event1', description: 'description1'}, {name:'event2', description: 'description2'},
{name:'event3', description: 'description3'}, {name:'event4', description: 'description4'},
{name:'event5', description: 'description5'}, {name:'even6', description: 'description6'}, {name:'even7', description: 'description7'}];
var counter = 0;
this.isLarge = false;
$scope.isLargeContainer = function() {
if(counter === 0) {
this.isLarge = true;
counter++;
} else {
this.isLarge = false;
if(counter === 2) {
counter = 0;
} else {
counter++;
}
}
};
});
View:
<body ng-controller="MainCtrl">
<section ng-repeat="(key, eventModel) in eventModels" >
<div ng-init="isLargeContainer(eventModel)"></div> <!--the only way I found to call a function within the repeat directive.-->
<div ng-if="isLarge">
<!--Display large content -->
<p class='large'>my large event: {{eventModel.name}}, {{eventModel.description}}</p>
</div>
<div ng-if="!isLarge">
<!--Display content small-->
<p class='small'>{{eventModel.name}}</p>
</div>
</section>
</body>
Thanks a lot in advance!
Im using Anguler 1.3.3
I just solved your problem. You can find the solution here:
http://jsfiddle.net/anasfirdousi/46saqLmw/
Here is the HTML
var myApp = angular.module('myApp',[]);
function myController($scope){
$scope.msg = 'Learning Angular JS ng-class Directive';
$scope.menu = [ 'Menu Item 1','Menu Item 2','Menu Item 3','Menu Item 4'];
}
.large {
font-size:16px;
}
body{
font-size:12px;
}
<div ng-app="myApp" ng-controller="myController">
{{ msg }}
<ul>
<li ng-repeat="(key, value) in menu" ng-class="{'large': $index==2 }"> {{ value }} </li>
</ul>
</div>
In your case, you actually have to use ng-class with expressions. The class is applied only if the condition holds true.
http://jsfiddle.net/anasfirdousi/cuo6cn3L/
Check the above link. This is another example if you want to do it on any specific condition. You can call a function which checks a condition and returns either a true or a false rather than using an inline expression.
var myApp = angular.module('myApp',[]);
function myController($scope){
$scope.msg = 'Learning Angular JS ng-class Directive';
$scope.menu = [ 'Menu Item 1','Menu Item 2','Menu Item 3','Menu Item 4'];
$scope.CheckSometing = function(v){
if(v=='Menu Item 3'){
return true;
}
else
{
return false;
}
}
}
.large {
font-size:16px;
}
body{
font-size:12px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myController">
{{ msg }}
<ul>
<li ng-repeat="(key, value) in menu" ng-class="{'large': CheckSometing(value) }"> {{ value }} </li>
</ul>
</div>

Resources