How to get consolidated results from ng-repeat? - javascript

See this plunker.
<div ng-repeat="subCategory in subCategorys | filter:{tags:tag}:true | orderBy:'id'">
{{subCategory.id}} {{subCategory.name}} {{subCategory.tags}}
<br/><br/>
You are now seeing details of <span ng-init="subCats = subCats + ' ' + subCategory.name">{{subCats}}</span>
</div>
This HTML page shows a filtered result from an object. However, I want to display a consolidated result of the names after "You are now seeing details of" like for example, "You are now seeing details of jim tom". This consolidated list should appear after the element which has ng-repeat directive.
How can this be done?
Thanks

I made an updated plunker for you.
Please try to make your example plunker way more reduced to the specific problem in the future as this helps us to help you.
First I added the search binding as filter to the ng-repeat to make the filter workable:
<div ng-repeat="subCategory in subCategorys | filter:{tags:tag}:true | filter:{id:search} | orderBy:'id'">
To avoid executing the filter twice you can save the filter result directly into a scope variable by simply assinging it (in my example to subCategorysFilter):
<div ng-repeat="subCategory in subCategorysFilter = (subCategorys | filter:{tags:tag}:true | filter:{id:search} | orderBy:'id')">
I further changed your getAllFilteredNames() method to take a filter object as argument and made it loop through the results, build an array of the names and join them with a , as separation:
$scope.getAllFilteredNames = function(filter){
var names = [];
angular.forEach(filter, function(element){
names.push(element.name);
});
return names.join(", ");
};
This is now called outside the ng-repeat directive:
You are now seeing details of {{getAllFilteredNames(subCategorysFilter)}}
Have fun!
Update
Two possible solutions for getting a multilined output:
1 - You might change the line
<div>You are now seeing details of {{getAllFilteredNames(subCategorysFilter)}}</div>
to
<div>You are now seeing details of <span ng-bind-html="getAllFilteredNames(subCategorysFilter)"></span></div>
Then any html tags within the expression are compiled as html code. But there are meaningful reasons for angular disabling this feature by default. If your objects are editable by users you need to prevent them from breaking your design by escaping all html tags...
2 - But if you do not need to display the cosolidated information within a single string, you might simply use another ng-repeat combined with an <ul> like this:
<div>You are now seeing details of <br/>
<ul>
<li ng-repeat="subCategory in subCategorysFilter">{{subCategoryName}}</li>
</ul>
</div>
Just style your li accordingly to be displayed underneath each other and you're ready to go.

You can do this in your HTML by moving your consolidated list outside of the ngRepeat and calling the filter again:
<div ng-repeat="subCategory in subCategorys | filter:{tags:tag}:true | orderBy:'id'">
{{subCategory.id}} {{subCategory.name}} {{subCategory.tags}}
<br/><br/>
</div>
<div>
You are now seeing details of
<span ng-repeat="subCategory in subCategorys | filter:{tags:tag}:true | orderBy:'id'">
{{subCategory.name}}
</span>
</div>
The drawback to this approach is that you are calling the filter twice. A better alternative would be to set up a $watch in your parent controller and invoke the $filter manually. I.e. Save the filtered results in a scope variable. The benefit is that the filter is called half as many times and the scope variables you set up are visible to the original list and the consolidated list.
app.controller('ParentController', function($scope, $filter) {
$scope.subCategorys = [{...}];
$scope.tag = {...};
$scope.$watchCollection('subCategorys', function(newList){
//if the collection changes, create a new tag
//reference that is a copy of the old one to trigger
//the tag watch listener
if (newList)
$scope.tag = angular.copy($scope.tag);
});
$scope.$watch('tag', function(newTag){
// if tag changes, apply the filter,
// and save the result to a scope variable
if(newTag)
$scope.filteredList = $filter('filter')
($scope.subCategories, { tags: newTag}, true);
});
});
HTML
<div ng-controller="ParentController">
<div ng-repeat="subCategory in filteredList | orderBy:'id'">
{{subCategory.id}} {{subCategory.name}} {{subCategory.tags}}
<br/><br/>
</div>
<div>
You are now seeing details of
<span ng-repeat="subCategory in filteredList | orderBy:'id'">
{{subCategory.name}}
</span>
</div>
</div>

I'm afraid there is no way of doing that except for selecting the subCategory back. Fortunately, there is a pretty elegant 'angular' way of doing that. Add this to your controller:
$scope.getSubCatById = function(someId) {
return $filter('filter')($scope.subCategorys, {id:someId})[0];
}
And then your html:
<div ng-repeat="subCategory in subCategorys | filter:{tags:tag}:true | orderBy:'id'">
{{subCategory.id}} {{subCategory.name}} {{subCategory.tags}}
<br/><br/>
You are now seeing details of {{ getSubCatById(2).name }}
</div>
I hope I interpreted your question correctly.

Related

How can I show comments of clicked feed only

I'm stuck in a situation, in a nested loop. The top loop iterating over the Feeds array and every feed in Feeds have comments array. This comments is used for inner loop.
My problem is, I have Comment button and on click of that button, I want to show the list of comments only for that feed post. But instead of showing comments on current feed post, it is showing comments for all other feeds also, that's because of use of only one Boolean variable showComments created inside .ts file.
So how can make that work for only the given context Feed post?
HTML
<ng-container *ngFor="let feed of feeds">
<div class="ed-card ed-card-feed">
<div class="ed-card-feed--footer">
<div class="card-feed-values">
<a class="mr-4"><span>{{feed.likes_count}}</span> Likes</a>
<a><span>{{feed.comments_count}}</span> Comments</a>
</div>
<div class="card-feed-actions">
<a class="mr-5" (click)="likePost(feed.id)"><span></span> Like</a>
<span></span> Comment button
</div>
</div>
// Below div needs to be open for that particular feed only
<div *ngIf="showComments" class="ed-card-feed--comments">
<div class="feed-comments-list">
<ng-container *ngFor="let comment of feed.comments?.edges">
<div class="comment-list-item">
<p class="comment-box mt-1">
{{comment.node?.body}}
</p>
</div>
</ng-container>
</div>
</div>
</div>
</ng-container>
P.S. - Please don't confuse with feed.comments?.edges that's just because of GraphQL. It is just an array.
Thanx
You can maintain an array of showComments[] equal to size of feeds with initially set to false. showComments[false,false...]
<ng-container *ngFor="let feed of feeds; let myIndex = index"">
//code here
<span></span> Comment button
//check if true
<div *ngIf="showComments[myindex] === true" class="ed-card-feed--comments">
</ng-container>
Instead of a boolean, you can define a selectedFeeds array:
selectedFeeds = new Array<Feed>();
In the template, you add the feed that has been clicked to selectedFeeds:
<a href="javascript:void(0)" class="mr-5" (click)="selectedFeeds.push(feed)>Comment button</a>
and you filter the comments according to the selected feeds:
<div *ngIf="selectedFeeds.indexOf(feed) >= 0" class="ed-card-feed--comments">
The best way to achieve this is to separate the code to different components. And since each of the component will have it's own instance of comments - you won't have a problem creating a local variable and showing/displaying the comments in there.
I've created an example of how it can be implemented: stackblitz
Another approach
<ng-container *ngFor="let feed of feeds">
Comment button
<div *ngIf="feed.showComments" class="ed-card-feed--comments">
</ng-container>
Js file
toggleComments(feed) {
feed.showComments = true // (feed.showComments = !feed.showComments => to show or hide (toggle))
}

If all array items are hidden show another div in AngularJS

I'm using ng-repeat on my page. ng-class working very well.
<div class="card news" ng-repeat="item in news track by $index" id="{{news.nid}}" ng-init="parentIndex = $index" ng-class="{hidden: '{{getCheck($index)}}' == 'true'}">
...
</div>
Now I need, if all items are hidden, show this div:
<h3 class="news-empty">No news</h3>
Whats the rules? How can I do it? Thanks.
You need another method that checks if all elements are hidden:
$scope.everythingIsHidden = function() {
return $scope.news.every((new, index) => $scope.getCheck(index));
}
$scope.getCheck = function(index) { // Your getChek function that I suppose it checks if an element is hidden based on index
//...
}
<h3 class="news-empty" ng-if="everythingIsHidden()">No news</h3>
TheCog's answer will work. If you want to do this in a more 'Angular' way you're going to need to refactor what you have.
You shouldn't be trying to hide them with a CSS class. ngRepeats have a built in filter syntax. So, you should be filtering them out.
<div class="card news"
ng-repeat="item in news | filterMethod as results track by $index"
id="{{news.nid}}"
ng-init="parentIndex = $index"
>
<h3 class="news-empty" ng-if="results.length === 0" >No news</h3>
The as results statement in the repeat will store the filtered array in results. filterMethodneeds to be an angular filter and it will probably work similarly to your getCheck($index) method.
You want to add an ngShow to the h3 tag, and aim it at a function you write in your controller that checks if the array is empty, probably by iterating over the same array that's hidden and running getCheck($index) on each.

Trigger function on a certain element - ngrepeat - angularjs

Good morning,
I'm trying to change the limitTo filter on a certain list, my issue is:
when I click to the trigger who change the filter limit the filter changes on all ng-repeated categories.
my function inside the main controller
$scope.showMore = function(limit) {
if($scope.limitItems === $scope.itemsPerList) {
$scope.limitItems = limit;
$scope.switchFilterText = 'less';
} else {
$scope.switchFilterText = 'more';
$scope.limitItems = $scope.itemsPerList;
}
}
my scenario (I rewrote it in a simplified version)
<li ng-repeat="item in category.items | limitTo: limitItems ">
{{item.title}}
</li>
<li ng-if="limitItems < (category.items.length)">
<a ng-click="showMore(category.items.length)" >Show {{ switchFilterText }}</a>
</li>
Could you explain me what's wrong with me?
I searched how to select a single element to apply the function but I didn't find anything useful
Update:
I found the way to solve my issue in this way:
No functions inside the controller are involved to make this functionality works properly:
<li ng-repeat="category in maincategories" ng-init="limitItems = maxItemsPerList">
{{category.title}}
<ul>
<li ng-repeat="item in category.items | limitTo: limitItems "> {{item.title}}
</li>
</ul>
<a ng-click="limitItems = category.items.length" href>
<b ng-if="category.items.length > maxItemsPerList && limitItems != category.items.length "> Show more </b>
</a>
I'm not really convinced about Angular (I used it in my past and I was impressed by the performance but now I can see logics senseless):
What I learned:
ng-if and ng-click cannot be used in the same content because ng-if creates new scopes so if you put ng-if on top of the "show more" link it will break the code
ng-init cannot be used in the same element of the ng-repeat otherwise the var initialised will not be available inside the ng-repeat block
I think there is another way to do that, maybe more clean but in this specific case I can't do a lot.
ng-if and ng-click cannot be used in the same content because ng-if
creates new scopes so if you put ng-if on top of the "show more" link
it will break the code
Yes, ng-if creates a new scope, but it is possible to mix ng-if and ng-click (and most other directives). To do that, you'll be safer if you always write to atributes of another object instead of a simple variable. It is plain JavaScript prototypal inheritance in play.
<li ... ng-init="category.limitItems = maxItemsPerList">
ng-init cannot be used in the same element of the ng-repeat otherwise
the var initialised will not be available inside the ng-repeat block
True, in the sense that variables are created in the local scope. But again, refer to an object.
I think there is another way to do that, maybe more clean but in this
specific case I can't do a lot.
You don't need to do a lot, it is quite simple to do it right actually.
Some advices:
Use ng-init with care. I know it will tempt us but always try to put logic inside controllers and services;
Avoid assignments inside templates;
Learn how to use controllerAs syntax. It gives you an object to write your models to (the controller), so solves most problems related to scope inheritance;
Do not inject $scope, put your view models inside controllers.
Full code goes like this:
<li ng-repeat="category in maincategories" ng-init="category.limitItems = maxItemsPerList">
{{category.title}}
<ul>
<li ng-repeat="item in category.items | limitTo: category.limitItems "> {{item.title}}
</li>
</ul>
<a ng-if="category.items.length > maxItemsPerList && category.limitItems != category.items.length" ng-click="category.limitItems = category.items.length" href>
<b> Show more </b>
</a>

$scope variables shortcuts in AngularJs Templates

In my controller I assign:
$scope.currentThing.data
And in my template sometimes I need
currentThing.data.response[0].hello
and sometimes
currentThing.data.otherStuff[0].goodbye
//or
currentThing.data.anotherThing[0].goodMorning
So I was wondering if it is possible to create a shortcut for this variables straight in the templates, something like:
{{response = currentThing.data.response[0]}}
So that I can use it like this {{response.hello}}.
In general, is it possible to assign temporary variables from the template? I don't need to have any data-binding, I would need them only for generating the template and then they can disappear forever
You can do that in controller like here: http://jsbin.com/moyuhe/1/edit
app.controller('firstCtrl', function($scope){
$scope.currentThing = {
data: [
{response:[
{hello:1},
{hello:2}
]}
]
};
$scope.temp = $scope.currentThing.data[0];
});
HTML:
<div ng-controller="firstCtrl">
{{temp.response |json }}
</div>
You might be able to use ngInit: https://docs.angularjs.org/api/ng/directive/ngInit
Although it seems that using it outside of ngRepeat is frowned upon.
<div ng-init="myvar = currentThing.data.response[0]">
<span>{{myvar.hello}}</span>
</div>
I haven't tested it like this but that's the closest thing I can think of that may solve your problem.
Yes, it is possible using syntax like
{{ variable = ( expression ) }}
anywhere in HTML template (not just ng-init as some suggest).
It is exceptionally useful in cases when you need to use a calculated variable more times - it does not need to be calculated each time.
Some example
<!-- can use it before -->
<p> Calculated value is {{calculated}} </p>
<div ng-repeat=" item in calculated = ( allItems | filter1 | filter2 | filter3 ) ">
{{item}}
</div>
<!-- can use it after-->
<p> Calculated value is still {{calculated}} </p>
Edit: so in your case
{{response = ( currentThing.data.response[0] ) }}

AngularJS - how to get an ngRepeat filtered result reference

I am using an ng-repeat directive with filter like so:
ng-repeat="item in items | orderBy:'order_prop' | filter:query | limitTo:4"
and I can see the rendered results fine; now I want to run some logic on that result in my controller. The question is how can I grab the result items reference?
Update:
Just to clarify: I'm trying to create an auto complete, I have this input:
<input id="queryInput" ng-model="query" type="text" size="30" placeholder="Enter query">
and then the filtered results:
<ul>
<li ng-repeat="item in items | orderBy:'order_prop' | filter:query | limitTo:4">{{item.name}}</li>
</ul>
now I want to navigate the result and select one of the items.
UPDATE: Here's an easier way than what was there before.
<input ng-model="query">
<div ng-repeat="item in (filteredItems = (items | orderBy:'order_prop' | filter:query | limitTo:4))">
{{item}}
</div>
Then $scope.filteredItems is accessible.
Here's the filter version of Andy Joslin's solution.
Update: BREAKING CHANGE. As of version 1.3.0-beta.19 (this commit) filters do not have a context and this will be bound to the global scope. You can either pass the context as an argument or use the new aliasing syntax in ngRepeat, 1.3.0-beta.17+.
// pre 1.3.0-beta.19
yourModule.filter("as", function($parse) {
return function(value, path) {
return $parse(path).assign(this, value);
};
});
// 1.3.0-beta.19+
yourModule.filter("as", function($parse) {
return function(value, context, path) {
return $parse(path).assign(context, value);
};
});
Then in your view
<!-- pre 1.3.0-beta.19 -->
<input ng-model="query">
<div ng-repeat="item in items | orderBy:'order_prop' | filter:query | limitTo:4 | as:'filteredItems'">
{{item}}
</div>
<!-- 1.3.0-beta.19+ -->
<input ng-model="query">
<div ng-repeat="item in items | orderBy:'order_prop' | filter:query | limitTo:4 | as:this:'filteredItems'">
{{item}}
</div>
<!-- 1.3.0-beta.17+ ngRepeat aliasing -->
<input ng-model="query">
<div ng-repeat="item in items | orderBy:'order_prop' | filter:query | limitTo:4 as filteredItems">
{{item}}
</div>
Which gives you access to $scope.filteredItems.
Try something like this, the problem with the ng-repeat is that it creates child scope because of that you can't access
filteritems
from the controller
<li ng-repeat="doc in $parent.filteritems = (docs | filter:searchitems)" ></li>
Update:
Even after 1.3.0. if you want to put it on the scope of the controller or the parent you cannot do that with as syntax.
For example if I have the following code:
<div>{{labelResults}}</div>
<li ng-repeat="label in (labels | filter:query | as labelResults)">
</div>
the above will not work.
The way to go around it is using the $parent as so:
<li ng-repeat="label in ($parent.labelResults = (labels | filter:query))">
I came up with a somewhat better version of Andy's solution. In his solution ng-repeat places a watch on the expression that contains the assignment. Each digest loop will evaluate that expression and assign the result to the scope variable.
The problem with this solution is that you might run into assignment issues if you are in a child scope. This is the same reason why you should have a dot in ng-model.
The other thing I don't like about this solution is that it buries the definition of the filtered array somewhere in the view markup. If it is used in multiple places in your view or your controller it can get confusing.
A simpler solution is to just place a watch in your controller on a function that makes the assignment:
$scope.$watch(function () {
$scope.filteredItems = $scope.$eval("items | orderBy:'order_prop' | filter:query | limitTo:4");
});
This function will be evaluated during each digest cycle so performance should be comparable with Andy's solution. You can also add any number of assignments in the function to keep them all in one place rather than scattered about the view.
In the view, you would just use the filtered list directly:
<ul>
<li ng-repeat="item in filteredItems">{{item.name}}</li>
</ul>
Here's a fiddle where this is shown in a more complicated scenario.
Check this answer:
Selecting and accessing items in ng-repeat
<li ng-repeat="item in ..." ng-click="select_item(item)">

Categories

Resources