I am displaying the list using ng-repeat and have set the limitTo for all elements. If user clicks the read more button then I want to increase the limitTo for that specific index. Problem is it changes the limitTo value for all elements in ng-repeat.
How to handle specific element by passing $index, any help ?
E.g.
<div class="review-story" ng-repeat="review in reviews">
{{review.story | limitTo:numLimit}}
<a class="readmore" ng-click="readMore($index)">read more...</a>
</div>
JS:
$scope.numLimit = 50;
$scope.readMore = function (index) {
if ($scope.numLimit == 50) {
$scope.numLimit = 10000;
$('.readmore').html("read less");
}
else {
$('.readmore').html("read more...");
$scope.numLimit = 50;
}
};
here is one solution which stores the limit in the data object. You may not want to do that, but then your solution is to store some data in the scope mapping reviews onto limit amounts.
http://plnkr.co/edit/spVkTCYZaBpZtQgAjrNi?p=preview
$scope.reviews = [
{
story: 'this is the first story'
},
{
story: 'this is the second story'
},
{
story: 'this is the third story and it is longer than 30 characters'
},
{
story: 'this is the fourth story and it is longer than 30 characters'
}
];
$scope.increaseLimit = function(review) {
if (review.limit) {
review.limit += 10;
}
else {
review.limit = 40;
}
}
Template:
<div class="review-story" ng-repeat="review in reviews">
{{review.story | limitTo: review.limit || 30}}<br />
{{review}}
click me
</div>
Related
I have this ng-repeat that I show split over three rows here for clarity:
ng-repeat="row in phs.phrasesView =
(phs.phrases | orderBy:phs.phrasesOrderBy[phs.phrasesOrderById]
.key:phs.phrasesSortDirectionId == 1)">
On my form I have a field phs.keywordRange
Is there a way that I can make it so the rows returned are filtered as follows:
When phs.keywordRange is null or empty string, all rows are shown
When phs.keywordRange is A then only rows where row.keyword starts with A are shown
When phs.keywordRange is ABC then only rows where row.keyword starts with ABC are shown
Make your own filter, like this fiddle.
Changing the $scope.keywordRange will update the list accordingly.
as shortcut:
.filter('keywordRange', function() {
return function(value, keyword) {
var out = [];
if(!keyword) {
return value;
}
for(var i = 0; i < value.length; i++) {
if(value[i].startsWith(keyword)){
out.push(value[i]);
}
}
return out;
};
});
function MyCtrl($scope) {
$scope.keywordRange = 'ti';
$scope.lines = [
'rest1', 'rest2', 'tiago', 'pedro', 'america'
];
}
and the html
<div ng-controller="MyCtrl">
<div ng-repeat="line in lines | keywordRange:keywordRange">
<p>{{line}}</p>
</div>
</div>
So I have an app that is updating data in real time using socket.io and displaying it with Angular JS.
I have it displaying data (comments) in multiple ng-repeats which are using ‘track by’ to ensure that duplicates are ignored when the latest data is brought in. I’m also using LimitTo to only show a certain amount of comments at a time, LimitTo is dynamic and is increased when the user clicks a button.
The HTML
<div ng-controller="CommentsController" >
<!-- Comment Repeater Starts Here -->
<div ng-repeat="comment in comments | limitTo: limit track by comment.id" >
{{ comment.comment }}
<!-- Nested Reply Comments Start Here -->
<div ng-repeat="reply in comment.replies | limitTo: comment.limit track by reply.id" >
<div class="comment-text" >
{{ reply.comment }}
</div>
</div>
<a href="#" ng-click="increaseReplyLimit(comment, comment.limit)" ng-show="hasMoreComments(comment.replies, comment.limit)" >View More Replies</a>
</div>
<a href="#" ng-click="increaseLimit(limit)" ng-show="hasMoreComments(comments,limit)" >View More Comments</a>
It works perfectly fine for my first ng-repeat, because I assign LimitTo to a variable on the scope, which is unaffected when I bring in new data through Socket.io. For my nested ng-repeat, though, I am using comment.limit as the variable for the LimitTo and this gets overwritten every single time I bring in the new data through socket.io.(the new data has a default for comment.limit - i tried leaving this blank before but then nothing shows).
The Angular
app.controller('CommentsController', function ($scope,socket,$http,$location) {
// fetching the latest comments from the API location
$http.get( $url + 'comments').success(function(comments) {
if (comments) {
$scope.comments = comments;
}
});
// updating comments via socket.io
socket.on('comment.update', function (data) {
$scope.comments = JSON.parse(data);
});
$scope.limit = 2;
$scope.hasMoreComments = function(comments, limit) {
if (typeof comments != "undefined" && comments != "false") {
var $commentLength = comments.length;
if ($commentLength > limit) {
return true;
}
return false;
}
return false;
}
$scope.increaseLimit = function(limit) {
$scope.limit = $scope.limit + 2;
}
$scope.increaseReplyLimit = function(comment, limit) {
comment.limit = parseInt(limit) + 2;
}
});
How can I prevent the current limit for each nested repeat from getting overwritten when I bring in new data from socket.io?
I already tried doing a deep merge on both the old and new data (with the idea of updating the nested limit in the new data to reflect the current nested limit). However, when i did that, Angular completely ignored the new limit and no longer enforced any limit for nested comments.
Structure of the data being brought in
[
{
"topics": [
{
"id": 75,
"topic": "test",
"approved": 1,
"created_at": "-0001-11-30 00:00:00",
"updated_at": "-0001-11-30 00:00:00",
"slug": "test",
"blurb": null
}
],
"id": 849,
"user_id": 80,
"news_id": 9,
"context": "Test News Article 1",
"comment": "<p>test comment 4</p>",
"origin": "Test",
"origin_url": "http://localhost:8000/news/test-news-article-1",
"author": "omurphy27",
"author_url": null,
"votes": 0,
"created_at": "2015-06-08 22:36:53",
"updated_at": "2015-06-08 22:36:53",
"approved": 1,
"slug": "test-comment-116",
"original": 1,
"parent_id": null,
"username": "omurphy27",
"limit": 2,
"voted": false
}
]
Been banging my head against the wall for awhile with this one and any help is much appreciated.
I ended up creating a separate array for my nested reply limits and attaching it to the scope (rather than having these limits in the data object that was being brought in and updated in realtime by socket.io )
See my updated angular code below
app.controller('CommentsController', function ($scope,socket,$http,$location) {
// fetching the latest comments from the API location
$http.get( $url + 'comments').success(function(comments) {
if (comments) {
$scope.comments = comments;
}
// populate my separate array for reply limits
$scope.populateLimits(comments.length, 4);
});
$scope.populateLimits = function(limitLength, limit) {
var limits = [];
for (var i = limitLength - 1; i >= 0; i--) {
limits[i] = limit;
};
$scope.limits = limits;
}
// updating comments via socket.io
socket.on('comment.update', function (data) {
if (data.length > $scope.comments.length) {
var difference = data.length - $scope.comments.length;
// increment the limits array by however
// many new comments are pulled in by socket.io
$scope.incrementLimits(difference);
}
$scope.comments = JSON.parse(data);
});
$scope.incrementLimits = function(difference) {
var newLimits = $scope.limits;
for (var i = 0; i < difference; i++) {
newLimits.unshift(2);
};
$scope.limits = newLimits;
}
$scope.limit = 2;
$scope.hasMoreComments = function(comments, limit) {
if (typeof comments != "undefined" && comments != "false") {
var $commentLength = comments.length;
if ($commentLength > limit) {
return true;
}
return false;
}
return false;
}
$scope.increaseLimit = function(limit) {
$scope.limit = $scope.limit + 2;
}
$scope.increaseReplyLimit = function(limit,index) {
$scope.limits[index] = parseInt(limit) + 2;
}
});
I populate this separate limits array based on how many 'parent' comments I have and then I increment it by however many new comments are brought in. Right now I'm just showing comments based in order, with the newest shown first, so I don't have to track which 'limits' belong to which comment; I can simply add new ones to the beginning of the array.
As for accessing the $scope.limits array in my nested ng-repeat and using it as the LimitTo variable, I do so using the following variable: limits[$index].
See my updated markup below:
<div ng-controller="CommentsController" >
<!-- Comment Repeater Starts Here -->
<div ng-repeat="comment in comments | limitTo: limit track by comment.id" >
{{ comment.comment }}
<!-- Nested Reply Comments Start Here -->
<div ng-repeat="reply in comment.replies | limitTo: limits[$index] track by reply.id" >
<div class="comment-text" >
{{ reply.comment }}
</div>
</div>
<a href="#" ng-click="increaseReplyLimit(limits[$index], $index)" ng-show="hasMoreComments(comment.replies, limits[$index])" >View More Replies</a>
</div>
<a href="#" ng-click="increaseLimit(limit)" ng-show="hasMoreComments(comments,limit)" >View More Comments</a>
</div>
The above fixes the issue I was having where the dynamic variable I was using for the limitTo for my nested ng-repeat kept getting overwritten. I do need to have the $scope.limits sync up better with my data, but I hope this helps anyone else who was encountering the same issue.
Cheers
I want to make some kind of project list from a JSON file. The data structure (year, month, project) looks like this:
[{
"name": "2013",
"months": [{
"name": "May 2013",
"projects": [{
"name": "2013-05-09 Project A"
}, {
"name": "2013-05-14 Project B"
}, { ... }]
}, { ... }]
}, { ... }]
I'm displaying all data using a nested ng-repeat and make it searchable by a filter bound to the query from an input box.
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.projects | filter:query | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
If I type "Project B" now, all the empty parent elements are still visible. How can I hide them? I tried some ng-show tricks, but the main problem seems so be, that I don't have access to any information about the parents filtered state.
Here is a fiddle to demonstrate my problem: http://jsfiddle.net/stekhn/y3ft0cwn/7/
You basically have to filter the months to only keep the ones having at least one filtered project, and you also have to filter the years to only keep those having at least one filtered month.
This can be easily achieved using the following code:
function MainCtrl($scope, $filter) {
$scope.query = '';
$scope.monthHasVisibleProject = function(month) {
return $filter('filter')(month.children, $scope.query).length > 0;
};
$scope.yearHasVisibleMonth = function(year) {
return $filter('filter')(year.children, $scope.monthHasVisibleProject).length > 0;
};
and in the view:
<div class="year" ng-repeat="year in data | filter:yearHasVisibleMonth | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:monthHasVisibleProject | orderBy:sortMonth:true">
This is quite inefficient though, since to know if a year is accepted, you filter all its months, and for each month, you filter all its projects. So, unless the performance is good enough for your amount of data, you should probably apply the same principle but by persisting the accepted/rejected state of each object (project, then month, then year) every time the query is modified.
I think that the best way to go is to implement a custom function in order to update a custom Array with the filtered data whenever the query changes. Like this:
$scope.query = '';
$scope.filteredData= angular.copy($scope.data);
$scope.updateFilteredData = function(newVal){
var filtered = angular.copy($scope.data);
filtered = filtered.map(function(year){
year.children=year.children.map(function(month){
month.children = $filter('filter')(month.children,newVal);
return month;
});
return year;
});
$scope.filteredData = filtered.filter(function(year){
year.children= year.children.filter(function(month){
return month.children.length>0;
});
return year.children.length>0;
});
}
And then your view will look like this:
<input type="search" ng-model="query" ng-change="updateFilteredData(query)"
placeholder="Search..." />
<div class="year" ng-repeat="year in filteredData | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Example
Why not a custom $filter for this?
Efficiency: the nature of the $diggest cycle would make it much less efficient. The only problem is that this solution won't be as easy to re-use as a custom $filter would. However, that custom $filter wouldn't be very reusable either, since its logic would be very dependent on this concrete data structure.
IE8 Support
If you need this to work on IE8 you will have to either use jQuery to replace the filter and map functions or to ensure that those functions are defined, like this:
(BTW: if you need IE8 support there is absolutely nothing wrong with using jQuery for these kind of things.)
filter:
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
'use strict';
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
map
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(" this is null or not defined");
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (thisArg) {
T = thisArg;
}
A = new Array(len);
k = 0;
while(k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[ k ];
mappedValue = callback.call(T, kValue, k, O);
A[ k ] = mappedValue;
}
k++;
}
return A;
};
}
Acknowledgement
I want to thank JB Nizet for his feedback.
For those who are interested: Yesterday I found another approach for solving this problem, which strikes me as rather inefficient. The functions gets called for every child again while typing the query. Not nearly as nice as Josep's solution.
function MainCtrl($scope) {
$scope.query = '';
$scope.searchString = function () {
return function (item) {
var string = JSON.stringify(item).toLowerCase();
var words = $scope.query.toLowerCase();
if (words) {
var filterBy = words.split(/\s+/);
if (!filterBy.length) {
return true;
}
} else {
return true;
}
return filterBy.every(function (word) {
var exists = string.indexOf(word);
if(exists !== -1){
return true;
}
});
};
};
};
And in the view:
<div class="year" ng-repeat="year in data | filter:searchString() | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:searchString() | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | filter:searchString() | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Here is the fiddle: http://jsfiddle.net/stekhn/stv55sxg/1/
Doesn't this work? Using a filtered variable and checking the length of it..
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true" ng-show="filtered.length != 0">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in filtered = (month.projects | filter:query) | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
looking for some ideas here. i have a meal plan object that contains an array of meals. only one meal can be set as primary at a time but i want the user to be able to cycle through the array of meals and mark a meal as primary. i am stuck trying to figure out if ngrepeat makes sense here or ngswitch or ngshow. any thoughts or samples would be highly appreciated!
I have tried multiple approaches with no luck.
thanks
You could cycle through the meals by index of the meal and have a button to choose the meal like this:
http://jsfiddle.net/c6RZK/
var app = angular.module('mealsApp',[]);
app.controller('MealsCtrl',function($scope) {
$scope.meals = [
{name:'Meatloaf'},
{name:'Tacos'},
{name:'Spaghetti'}
];
$scope.meal_index = 0;
$scope.meal = {};
$scope.next = function() {
if ($scope.meal_index >= $scope.meals.length -1) {
$scope.meal_index = 0;
}
else {
$scope.meal_index ++;
}
};
$scope.choose = function(meal) {
$scope.meal = meal;
}
});
HTML
<div ng-app="mealsApp" ng-controller="MealsCtrl">
<div ng-repeat="m in meals">
<div ng-if="meal_index == $index">
<strong>{{m.name}}</strong>
<button ng-click="choose(m)">Choose</button>
</div>
</div>
<hr>
<button ng-click="next()">Next</button>
<hr>Your Choice: {{meal.name}}
</div>
You could just attach a property to the plan, with a flag that says whether or not it's the primary plan.
Here's a sample implementation:
$scope.plans = [{name:"One"}, {name:"Two"}, {name:"Three"}];
$scope.selectPlan = function(plan) {
for(var i = 0, l = $scope.plans.length; i < l; i++) {
$scope.plans[i].primary = false;
if($scope.plans[i] === plan) {
$scope.plans[i].primary = true;
}
}
};
HTML:
<ul>
<li ng-click="selectPlan(plan)" ng-repeat="plan in plans" ng-class="{primary: plan.primary}"><a href>{{plan.name}}</a></li>
</ul>
If you'd rather not attach properties you could use something like a selected index property on your controller.
I Have an array of items like this which contains a list of animals and a list of fruits in a random order.
$scope.items = [{name:'mango',type:'fruit'},{name:'cat',type:'animal'},{name:'dog',type:'animal'},{name:'monkey',type:'animal'},{name:'orange',type:'fruit'},{name:'banana',type:'fruit'},...]
Then I Have a array of colors like
$scope.colorSeries = ['#3366cc', '#dc3912', '#ff9900',...];
$scope.setBGColor = function (index) {
return { background: $scope.colorSeries[index] }
}
I am using the items array to render the fruits only in a div with background color selected from the colorSeries based on the index like colorSeries[0] which will give me #3366cc
<div data-ng-repeat="item in items " ng-if="item.type =='fruit'">
<label ng-style="setBGColor($index)">{{item.name}}</label>
</div>
Things working fine if the length of the items array is less than length of colorSeries array.The problem arises if the length of colorSeries array is less than the items array.e.g if i have a color series with 3 colors then for this items array the last item i.e orange will need a color indexed as colorSeries[4] which is undefined where as I have rendered only three items. So, is it possible to get the index like 0,1,2 i.e the index of elements rendered with ng-if.
Instead of using ng-if, I would use a filter. then, the $index will be always correspond to the index in the result list after applying the filter
<div data-ng-repeat="item in items|filterFruit">
<label ng-style="setBGColor($index)">{{item.name}}</label>
</div>
angular.module('app.filters', []).filter('filterFruit', [function () {
return function (fruits) {
var i;
var tempfruits = [];
var thefruit;
if (angular.isDefined(fruits) &&
fruits.length > 0) {
for(thefruit = fruits[i=0]; i<fruits.length; thefruit=fruits[++i]) {
if(thefruit.type =='fruit')
tempfruits.push(thefruit);
}
}
return tempfruits;
};
}]);
Try this..
this.itemList = [{
name:'apple'
}, {
name: 'fruit'
}];
this.colorlist = [{ color: 'red' }, { color: 'orange' }, { color: 'blue' }];
<div data-ng-repeat="item in itemList " ng-if="item.name=='fruit'">
<label ng-style="colorlist [$index]">{{item.name}}</label>
</div>
If I were you, I would embed the colors inside the $scope.items objects, since you always use them coupled with a fruit.
Anyway, to address your specific code configuration, I would add a counter in my controller and use it to loop through the colors.
Something like this:
app.controller('myCtrl', function ($scope) {
$scope.colorSeries = ['#3366cc', '#dc3912', '#ff9900',...];
var colorCounter = $scope.colorSeries.length;
var colorIdx = -1;
$scope.setBGColor = function () {
// get back to the first color if you finished the loop
colorIdx = colorCounter? 0: colorIdx+1
return { background: $scope.colorSeries[colorIdx] }
}
})
And then in your view (note that there is no $index)
<div data-ng-repeat="item in items " ng-if="item.type =='fruit'">
<label ng-style="setBGColor()">{{item.name}}</label>
</div>