Angularjs directive to append to template - angularjs

I am new to angularjs world and am trying to do something that I think should be achievable with a directive.
I have a template which has a list of articles listed using ng-repeat. These articles have a date on them. I want to group the articles by date in the template. So I am thinking of creating a directive that would append a new div before each group of articles in that day. The data in the model is already sorted by date desc.
Should I be using the compile function in the directive to do this ? Any code examples would be great.

If I understand you correctly you want the output to be something like:
<ul>
<li>
Show 3 articles for date 2012-12-07
</li>
<li>
Show 1 articles for date 2012-12-06
</li>
<li>
Show 2 articles for date 2012-12-05
</li>
</ul>
In that case, I would do the grouping before it renders:
function ArticlesController ($scope) {
var groupArticles = function (articles) {
var i,
art = {};
for (i = 0; i < articles.length; i += 1) {
if (!art.hasOwnProperty(articles[i].date)) {
art[articles[i].date] = [];
}
art[articles[i].date].push(articles[i]);
}
return art;
};
$scope.articles = [{ date: '2012-12-07', title: 'Marcus' },
{ date: '2012-12-07', title: 'Zero' },
{ date: '2012-12-06', title: 'Moxxi' },
{ date: '2012-12-05', title: 'Dr Zed' }];
$scope.groupedArticles = groupArticles($scope.articles);
}
And you view:
<ul data-ng-controller="ArticlesController">
<li data-ng-repeat="articles in groupedArticles">
<div data-ng-repeat="article in articles">
{{ articles.title }}
</div>
</li>
</ul>

<ul>
<li ng-repeat="article in articles">
<ng-switch on="$first || article.date != articles[$index-1].date">
<div ng-switch-when="true" class="group_heading">{{article.date}}</div>
</ng-switch>
{{article.title}}
</li>
</ul>
The above is modeled off an existing fiddle I had.
The above assumes (as you stated) that articles is already sorted. If not, the fiddle shows how to use the orderByFilter in a controller to create a sorted array based on any object property.

Related

AngularJS - Custom filter not working

I want to find the names, starting with the given letter i.e 'm' using angular's custom filter. Here I should get 2 entries in solutions but my view part shows only two bullets of an unordered list and not showing the names that starts with 'm'.
This is my filter:
arg.filter("startwith",function()
{
return function(input,option)
{
var op = [];//var s="";
for(var i=0;i<input.length;i++)
{
if(input[i].sname.charAt(0) == option)
op.push(input[i].sname);
}
// $scope.op;
// s=op.toString();
return op;
}
});
This is controller:
$scope.stud = [
{sid:1,sname:'john',gender:1,type:"fulltime"},
{sid:2,sname:'ashish',gender:1,type:"fulltime"},
{sid:3,sname:'naina',gender:2,type:"parttime"},
{sid:4,sname:'mitesh',gender:3,type:"fulltime"},
{sid:5,sname:'mithila',gender:2,type:"parttime"}
]
This is the view:
<h3> Names Start with M</h3>
<ul>
<li ng-repeat="s in stud | startwith : 'm'">
{{stud.sname}}
</li>
</ul>
It is because in your custom filter you are not returning objects but returning strings therefore the following would return empty as your strings would not have property of sname
<ul>
<li ng-repeat="s in stud | startwith : 'm'">
{{s.sname}}
</li>
</ul>
changing {{s.sname}} line to {{s}} would give you results.
<ul>
<li ng-repeat="s in stud | startwith : 'm'">
{{s}}
</li>
</ul>
Demo

Compare two lists in angularJs

I have two list that creat from two different json object:
<ul ng-repeat="a in user.data">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
and :
<ul ng-repeat="x in job.data">
<li>
<md-checkbox>
{{ x.user.name }}
</md-checkbox>
</li>
</ul>
I have to compare two lists and remove {{ a.name }} that the same as {{ job.user.name }} . Comparing json objects that have different structure is hard. How can I compare this two list and remove repeated items?
You can use the filter filter ;) using a function instead of a string in the expression argument. Remember this is the syntax of filter
{{ filter_expression | filter : expression : comparator}}
The expression can be a string, an object or a function. Something like this will do
$scope.filterUnemployed = function(value) {
var jobs = $scope.job.data;
for (var i = 0; i < jobs.length; i++) {
// Search every object in the job.data array for a match.
// If found return false to remove this object from the results
if (jobs[i].user.name === value.name) {
return false;
}
}
// Otherwise return true to include it
return true;
}
And then apply the filter to your ng-repeat directive like this
<ul ng-repeat="a in user.data | filter:filterUnemployed">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
Note that this will not modify your original collection but will result in a new copy beign displayed in your html since this is usually the desired effect.
Check the sample for a working demo
angular.module('app', [])
.controller('SampleCtrl', function($scope) {
$scope.user = {
data: [{
name: 'John'
}, {
name: 'Mary'
}, {
name: 'Peter'
}, {
name: 'Jack'
}, {
name: 'Richard'
}, {
name: 'Elizabeth'
}]
};
$scope.job = {
data: [{
jobTitle: 'CEO',
user: {
name: 'John'
}
}, {
jobTitle: 'CFO',
user: {
name: 'Mary'
}
}, {
jobTitle: 'Analist',
user: {
name: 'Jack'
}
}]
};
$scope.filterUnemployed = function(value) {
var jobs = $scope.job.data;
for (var i = 0; i < jobs.length; i++) {
if (jobs[i].user.name === value.name) {
return false;
}
}
return true;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="SampleCtrl">
<h1>All Users</h1>
<ul ng-repeat="a in user.data">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
<h1>Unemployed users</h1>
<ul ng-repeat="a in user.data | filter:filterUnemployed">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
<h1>Employed</h1>
<ul ng-repeat="x in job.data">
<li>
<md-checkbox>
{{ x.user.name }}
</md-checkbox>
</li>
</ul>
</div>
You can create a new array made from filtering one of your arrays :
var newArray = job.data.filter(function(jobData) {
return user.data.some(function(userData) {
return userData.name === jobData.user.name;
});
});
Something like this should help :
for(i=0; i<user.data.length;i++){
for(j=0; j<job.data.length;j++){
if(user.data[i].name === job.data[j].name){
user.date.splice(i,1);
}
}
}
I would appreciate some feedback, at least to know that my code helped you
You can use vanilla javascript, jQuery or library undescore.
Please check related links:
How can I merge two complex JSON objects with only unique or different values only showing in resultant array
How to merge two object values by keys
How to merge two arrays of JSON objects - removing duplicates and preserving order in Javascript/jQuery?
Merging two json objects in Java script?

How to detect my position in an ng-repeat loop?

I want to output a list of <li> elements using ng-repeat="obj in links", where links is an array of objects with href and text properties:
$scope.links = [
{ href: '/asdf', text: 'asdf'},
{ href: '/qwer', text: 'qwer'},
/* etc. */
{ href: '/zxcv', text: 'zxcv'}
];
But I want the ng-repeat loop to change what it does when it reaches a certain object in that array. Specifically, I want the loop to create hyperlinks for every object until obj.href==location.path() -- and after that, I just want to write out the text inside a <span>.
Currently, I'm solving this by creating both links and spans each time in the loop:
<ul>
<li ng-repeat="obj in links" ng-class="{active: location.path()==obj.href}">
<a ng-href="{{obj.href}}">{{obj.text}}</a>
<span>{{obj.text}}</span>
</li>
</ul>
plunkr
I then use CSS to hide all hyperlinks after the active class and hide all spans before it. But I don't want to just hide the links after the condition matches -- I want them to be completely removed from the DOM.
So there are two things you must do.
Find the index of the active element
Only show links up to the active index, and after that only show spans
What about this:
In your controller
$scope.lastIndex = 0;
$scope.$watch('links', function(newVal, oldVal){
for(var i=0; i< newVal.length; i++){
if (newVal[i].href == location.path()){
$scope.lastIndex = i
break;
}
}
}
In your HTML :
<ul>
<li ng-repeat="obj in links">
<a ng-if="$index <= {{lastIndex}}" ng-href="{{obj.href}}">{{obj.text}}</a>
<span ng-if="$index > {{lastIndex}}">{{obj.text}}</span>
</li>
</ul>
please see that example http://jsbin.com/cifef/1/edit
for your solution you need to replace $scope.location.href by location.path()
$scope.isLast = false;
$scope.getValue = function(obj)
{
if( obj.href==$location.path() || $scope.isLast )
{
$scope.isLast = true;
obj.isLast = true;
}
};
HTML:
<ul>
<li ng-repeat="obj in links" ng-class="{active: location.href==obj.href}" ng-init="getValue(obj)">
<a ng-href="{{obj.href}}" ng-hide="obj.isLast">{{obj.text}}</a>
<span ng-show="obj.isLast">{{obj.text}}</span>
</li>
</ul>

How to perform an action when a filtered collection changes

I have 3 connected lists that need to work a bit like cascading dropdown lists, that is, selecting an item in the first list filters the second which filters the third.
To achieve this I am using angular filters like so:
<div ng-app="" ng-controller="DemoCtrl">
<h3>Categories</h3>
<ul>
<li ng-repeat="cat in model.categories"
ng-click="selectCategory(cat)"
ng-class="{ active: cat === selectedCategory }">{{ cat }}</li>
</ul>
<h3>Sub Categories</h3>
<ul>
<li
ng-repeat="cat in model.subCategories | filter: { parent:selectedCategory }"
ng-click="selectSubCategory(cat)"
ng-class="{ active: cat.name === selectedSubCategory }">{{ cat.name }}</li>
</ul>
<h3>Products</h3>
<ul>
<li ng-repeat="product in model.products | filter:{ category:selectedSubCategory }">{{ product.name }}</li>
</ul>
</div>
When the top level category changes (selectCategory) I need to ensure that the first sub category is also selected:
$scope.selectCategory = function (cat) {
$scope.selectedCategory = cat;
// how to select the first sub category?
};
Since setting $scope.selectedCategory updates the filtered sub categories, is there anyway I can be notified when the filtered collection changes so I can select the first item ($scope.selectSubCategory)?
http://jsfiddle.net/benfosterdev/dWqhV/
You could set up a watcher on $scope.selectedCategory and manually run $filter to get the first sub-category.
In the end I opted for just performing the filtering in my controller and binding to a "filtered" object on my $scope. When the primary category changed we re-filter the subcategories and select the first item:
$scope.selectCategory = function (cat) {
$scope.model.selectedCategory = cat;
var filtered = $scope.getSubCategoriesOf(cat);
$scope.model.filteredSubCategories = filtered;
$scope.selectSubCategory(filtered[0]);
}
$scope.getSubCategoriesOf = function (cat) {
return $filter('filter')($scope.model.subCategories, { parent: cat }, true);
}

How do I only show an element if nested ng-repeat is not empty?

I have a List of lists, created with a nested ng-repeat. Each outer ng-repeat contains a div with the label of its inner list (eg: "Group A"). I'm now trying to create a way to avoid showing this label if the inner list is empty due to filtering(Applied by an input searchtext)
Here is a plunker explaining my issue and my attempted solution : Plnkr
Having a 'heavy' function like isGroupEmpty seems extremely cumbersome - Is there any way to do this in a much simpler fashion? I was toying with the idea of moving the label inside the inner ng-repeat and having ng-show="$first" but it doesnt look great
I ended up with the following solution which worked perfectly. Plnkr
By setting a variable in the inner ng-repeat I was able to evaluate ng-show based on this variables length like so :
<input ng-model='searchText'/>
<span ng-show='filtered.length > 0'>
<ul>
<li ng-repeat='el in filtered = (model | filter:searchText)'>
<div>{{el.label}}</div>
</li>
</ul>
</span>
you could leverage ng-init, that way you'll call the filter only once:
<div ng-repeat='(key,group) in model'>
<div ng-init="filtered = (group | filter:filterFn)"></div>
<div ng-show="filtered.length !== 0">
<div>{{key}}</div>
<ul>
<li ng-repeat="el in filtered">
<div>{{el.label}}</div>
</li>
</ul>
</div>
</div>
usually it is not a good practice to use ng-init out of no where, but I guess it solves calling the filter twice.
Another way is to use the filter through javascript - you could inject $filter and retrieve 'filter' $filter('filter') in your controller, calling it with group as its first argument, the filterFn as its second, and store its result in your scope.
I used the following:
<ul>
<li ng-repeat="menuItem in menuItems"><span class="fa {{menuItem.icon}} fa-lg"></span>{{menuItem.itemName}}
<span ng-show='menuItem.subItems.length > 0'>
<ul>
<li ng-repeat="subItem in menuItem.subItems">{{subItem.itemName}}</li>
</ul>
</span>
</li>
checking if an array has a length of 0 is not an expensive operation. if you want to only show lists that have item, put a filter on the outer array that takes an array of arrays and returns only the arrays that have a length different than 0.
you can also hide the inner div if the array == false.
http://plnkr.co/edit/gist:3510140
http://plnkr.co/edit/Gr5uPnRDbRfUYq0ILhmG?p=preview
Your plunkr was pretty complicated and hard to weed through so I re-created what you wanted using a fiddle. The general idea behind my approach is to filter out the items from the array, not the sub array. And only do the filtered items when the text changes. So here's the markup:
<div ng-app="app">
<div ng-controller="ParentCtrl">
<input data-ng-model="filterText" data-ng-change="updateTypes()" />
<div data-ng-repeat="type in filteredTypes">
{{ type.name }}
<ul>
<li style="margin-left:20px;" data-ng-repeat="entry in type.entries">
- {{ entry.name }}
</li>
</ul>
</div>
</div>
</div>
And here's the code:
angular.module('app', [])
function ParentCtrl($scope){
$scope.filterText = "";
$scope.types = [
{ name: "type1", entries: [{ name: "name1"}, { name: "name2"}, { name: "name3"}]},
{ name: "type2", entries: [{ name: "name1"}, { name: "name3"}, { name: "name3"}]},
{ name: "type3", entries: [{ name: "name1"}, { name: "name2"}, { name: "name5"}]},
{ name: "type4", entries: [{ name: "name4"}, { name: "name2"}, { name: "name3"}]}
];
$scope.filteredTypes = [];
$scope.updateTypes = function(){
$scope.filteredTypes.length = 0;
for(var x = 0; x < $scope.types.length; x++){
if($scope.filterText === ""){
$scope.filteredTypes.push($scope.types[x]);
}
else{
var entries = [];
for(var y = 0; y < $scope.types[x].entries.length; y++){
if($scope.types[x].entries[y].name.indexOf($scope.filterText) !== -1){
entries.push($scope.types[x].entries[y]);
}
}
if(entries.length > 0){
$scope.filteredTypes.push({
name: $scope.types[x].name,
entries: entries
});
}
}
}
}
$scope.updateTypes();
}
Fiddle: http://jsfiddle.net/2hRws/
The reason I'm creating a new array and not using an actual filter to remove the results is that angular doesn't like creating dynamic arrays on the fly in filters. This is because it doesn't assign $$hashKey and things just don't line up correctly when dirty checking. I got the idea of how to do what you needed from this topic on the matter: https://groups.google.com/forum/#!topic/angular/IEIQok-YkpU
I have only slightly modified your list-widget.html, see it in action: plunkr
The idea is simple - use the same filter for ng-show:
<div ng-show="group | filter:searchText">{{ key }}</div>
The label will be visible only if there are some unfiltered items.
In my example I'm using searchText for filter because I'm not familiar with CoffeeScript.

Resources