AngularJS ng-repeat, comma separated with 'and' before the last item - angularjs

I have a list of items ...
$scope.Users =[{
UserName: ''
}];
In my view I want to list them like this assuming I have only 4 items in my $scope:Users
Username1, Username2, Username3 and Username4
<span data-ng-repeat="user in Users">{{user.Username}}</span>{{$last ? '' : ', '}}
The above expression will basically add comma after each item and works fine.
My problem is how do I add an and keyword before the last item so it will be like:
Username1, Username2, Username3 and Username4
instead of:
Username1, Username2, Username3, Username4

$last is the truthy value.. so it holds either true or false and it doesn't hold the last element index..
I guess below expression should solve your problem
<p><span ng-repeat="user in Users">
{{user.Username}} {{$last ? '' : ($index==Users.length-2) ? ' and ' : ', '}}
</span></p>
Also make sure that you have the expression with $last within ng-repeat element and not outside of it
Please check the below working fiddle
http://jsfiddle.net/hrishi1183/Sek8F/2/

This could be one of the solutions
<span data-ng-repeat="user in Users">{{user.Username}}<font ng-show="!$last">,</font></span>

<span ng-repeat="user in Users">{{$first ? '' : $last ? ' and ' : ', '}}{{user.Username}}</span>
Instead of appending something prepend it. You can use $first to omit the first one. You can then use $last to add "and" instead of a comma.

Use a comma before and to promote clarity!
Bill Gates donates 6 million to Username1, Username2 and Username3.
vs
Bill Gates donates 6 million to Username1, Username2, and Username3.
Without the serial comma, the sentence does not clearly indicate that each of the users is to be given an equal share.
<span ng-repeat="user in Users">
{{user.Username}}{{$last ? '' : ($index==Username.length-2) ? ', and ' : ', '}}
</span>
Outputs:
Username1, Username2, Username3, and Username4

If you just need the text output, use a custom filter instead of ng-repeat:
<span>{{Users | humanizedUserList}}</span>
the filter code being something like this (using Lodash):
app.filter('humanizedUserList', function() {
return function(users) {
var last_users = _.last(users, 2);
return _.reduce(users, function(out, user, idx, users) {
out += user.UserName;
if(user === last_users[1]) { // last entry
return out;
}
else if(user === last_users[0]) { // second to last entry
return out + ', and ';
}
else {
return out + ', ';
}
});
};
}
You'd save yourself the hackish use of $last outside of the ng-repeat and ternary operators - and add reusability for other parts of your application.

I ran into this problem today, but with an extra challenge that the list items may need to be formatted within the sentence (for example, bold items or links). So just outputting a string from a filter wouldn't work. Initially I tried using ng-bind-html and outputting HTML from the filter, but that wasn't flexible enough.
I ended up making a directive that can be used within any ng-repeat to add the separator between list items. The tricky part is I only had access to $first, $last, and $index (not the original array or its length!).
My solution:
var app = angular.module('app', []);
app.directive('listSeparator', function() {
return {
replace: true,
template: '<span>{{ separator }}</span>',
link: function(scope, elem, attrs) {
scope.$watchGroup(["$index", "$first", "$last"], function() {
if (scope.$first)
scope.separator = "";
else if (scope.$last && scope.$index == 1)
scope.separator = " and ";
else if (scope.$last)
scope.separator = ", and ";
else
scope.separator = ",";
})
}
}
})
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
<div ng-app="app">
Click a button to see the formatted list.
<button ng-click="myArray = ['one']">One item</button>
<button ng-click="myArray = ['one','two']">Two items</button>
<button ng-click="myArray = ['one','two','three']">Three items</button><br>
<span ng-repeat="item in myArray"><list-separator></list-separator>
<strong>{{item}}</strong></span>
</div>
Enjoy!

Related

AngularJS - Using custom filter in ng-repeat for prefixing comma

Need to remove comma if value is empty works good if I have value
present at start or middle; But same doesn't work in this scenario.
app.filter('isCSV', function() {
return function(data) {
return (data !== '') ? data + ', ' : '';
};
});
Angularjs ng repeat for addressline - Plunker
I would instead operate on arrays of properties and use a pair of filters, one to remove empty values, and one to join the array.
This way it's very explicit about what properties you are displaying.
<body ng-controller="MainCtrl">
<ul>
<li ng-repeat="item in details">
{{ [ item.address0, item.address1, item.address2, item.address3] | removeEmpties | joinBy:', ' }}
</li>
</ul>
</body>
With the following filters:
app.filter('removeEmpties', function () {
return function (input,delimiter) {
return (input || []).filter(function (i) { return !!i; });
};
});
app.filter('joinBy', function () {
return function (input,delimiter) {
return (input || []).join(delimiter || ',');
};
});
Here's the updated Plunkr
Tricky but should work in your case Also no filter need
{{ item.address0 }} <span ng-if="item.address1">,
</span> {{ item.address1}}<span ng-if="item.address2">,</span>{{
item.address2}}
<span ng-if="item.address3">,</span>{{ item.address3}}
Here is working example
I would prefer writing a function instead of adding a filter so many times.
$scope.mergeAddresses = function(item) {
var address = item.address0;
[1,2,3].forEach(function(i) {
var add = item["address"+i];
if (!add) return;
address += (address ? ", " : "") + add;
});
if (address) address += ".";
return address;
}
Plunker

Can I repeat only in filter but keeping it to one element?

Hi I am wondering if I can use ng-repeat over an array but keeping it to 1 element but to show the repeated objects inside an attribute instead.
This is my current code:
<span ng-if='locations.length >= 2'>Several places</span>
<span ng-if='locations.length <= 1' ng-repeat="location in locations">{{location.city}}</span>
What I want to do now is if the locations.length is 2 or more elements I want this to show instead:
<span title='New York, Dallas, Los Angeles'>Several places</span>
I want my locations to repeat only inside the title attribute.
Now when I do like this:
<span ng-if='locations.length >= 2' ng-repeat="location in locations" title="{{location.city + ', '}}">Several pl..</span>
I get the following code which isnt wrong because that is what I've coded:
<span title="New York ,">Several places</span>
<span title="Dallas ,">Several places</span>
<span title="Los Angeles ,">Several places</span>
But it is not what I want my code to do, but I cannot really find how to do what I want to do. How can I achieve that my items in locations gets repeated only inside the title attribute?
Define a method in your controller that concatenates all city names as below
$scope.concatAllCityNames = function(locations) {
var allCityNames = "";
angular.forEach(locations, function(location ,index){
allCityNames = allCityNames + location.city;
if(index < locations.length-1) {
allCityNames = allCityNames + ", ";
}
});
return allCityNames;
}
call this method using AngularJS expression from title attribute as below
<span ng-if='locations.length >= 2' title="{{concatAllCityNames(locations)}}" >Several places</span>
Alternatively you can concatenate all city names in a $scope variable while retrieving locations and access it from title attribute as below
<span ng-if='locations.length >= 2' title="{{allCityNames}}" >Several places</span>
I think the best way to achieve what you want is to create a variable in the controller (since you didn't say anything about locations I made an assumption:
var locations = [
{city: 'New York'},
{city: 'Dallas'},
{city: 'Los Angeles'}
]
$scope.locationsTitle = locations.map(obj => obj.city).toString(); // 'New York,Dallas,Los Angeles'
Now you can use locationsTitle in the view to have what you asked :)
Just to give a variety of answers, you could also achieve what you're after with a directive. This example will make it flexible so that you can pass any collection of objects to the directive and specify which property on the objects to use for the title.
directive
.directive('elementWithTitle', function(){
return {
restrict: 'A',
scope: {
titleItems: '=',
titleItemProperty: '#'
},
link: function(scope, element, attrs) {
var title = '';
angular.forEach(scope.titleItems, function(item){
title += item[scope.titleItemProperty] + ', ';
});
// strip off last ,
title = title.substring(0, title.length - 2);
element.attr('title', title);
}
}
})
html
<span ng-if="locations.length >= 2" element-with-title title-items="locations" title-item-property="city">Several Places</span>

OrderBy array in AngularJS according to search

I'm still stuck with a OrderBy issue. Data comes from $http, and looks like this:
[
{
"number":1,
"timetable": [
{
"name":"Station1",
"time":"2016-05-18T18:14:00.000Z"
},
{
"name":"Station2",
"time":"2016-05-18T18:18:00.000Z"
}
]
},
{
"number":2,
"timetable": [
{
"name":"Station1",
"time":"2016-05-18T18:24:00.000Z"
},
{
"name":"Station2",
"time":"2016-05-18T18:28:00.000Z"
}
]
}
]
So what I need is to view rows where name is for example Station2, and I need them in order by time. The rows aren't ordered by time, nor by number. Amount of timetable rows varies, so row numbers don't help either. Is it possible to order them inside ng-repeat, in style of "OrderBy time where name='Station2' "?
EDIT:
At the moment I'm showing the results without any ordering, only with filtering. Current PHP:
<tr ng-repeat="x in rows | limitTo:5">
<td>
<a href="aikataulu.php?n={{x.number}}">
<span class="label label-primary line-{{x.lineID}}">{{x.lineID}}</span> {{x.number}}
</a>
</td>
<td>
<span ng-repeat="y in x.timetable | limitTo:-1">{{y.name}}</span> //This is for showing the destination
</td>
<td>
<span ng-repeat="y in x.timetable | filter:{'name':'<?php echo $as;?>'}: true | limitTo:1">{{y.time | date:'HH:mm'}}</span>
</td>
</tr>
$as is the station to be shown. So, now the order of the list comes straight from the JSON order, so it varies a lot.
You can use the comparator as an additional argument for the filter.
So you would expand your code to:
<span ng-repeat="y in x.timetable | filter:{'name':'<?php echo $as;?>'}: true : myComparator | limitTo:1">{{y.time | date:'HH:mm'}}</span>
You need a regexp to see if the compared string values are dates and you can do something similar to:
$scope.myComparator= function (a, b) {
// regex to see if the compared values are dates
var isDate = /(-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-)/g;
// sorting dates
if (isDate.test(a.value)) {
var aDate = new Date(a.value), bDate = new Date(b.value);
return aDate.getTime() < bDate.getTime() ? -1 : 1
}
// default sorting
return a.index < b.index ? -1 : 1
}
A thing worth noting is you would need a different regular expression to find your date format. Something along the lines of the following might be sufficient:
/(\d{4})-(\d{2})-/g
NOTE: This is untested code and serves only as a guide to the right approach. Make sure your version of AngularJS supports the comparator as a filter argument.
You can order an array of objects by one of their properties with a filter function like this one:
angular.module('yourApp').filter('orderObjectBy', function() {
return function(items, field, reverse) {
var filtered = [];
angular.forEach(items, function(item) {
filtered.push(item);
});
filtered.sort(function (a, b) {
return (a[field] > b[field] ? 1 : -1);
});
if(reverse) filtered.reverse();
return filtered;
};
}
);
Use it like this
<div ng-repeat="elem in data | orderObjectBy:'number'">
// ...
</div>

Postfix a comma if angularJS expression is not null

I need to display an address on the View. Something like
123 Street, 12 Apartment, 1 Floor, Richardson, Texas, 75081
User may skip entering the value of Floor for instance, and in that case I'll have to remove the comma visible after floor. Need something like -
{{streetAddress + ", "}}{{apartment + ", "}}{{floor + ", "}}{{city + ","}}{{state + ","}}{{zipCode}}
As in comma should be displayed only that expression has some value and is not null.
In the above statement if Floor is empty, it would display - Street, null, Apartment...
I am trying to avoid tedious writing like
<span ng-show="streetAddress">{{streetAddress}}, </span>
<span ng-show="floor">{{floor}}, </span>
and so on..
If the case of needing the comma is just isolated to this address the controller you should be enough. However, if you could think of a case where you would want to share this functionality throughout your app I would suggest a filter to handle adding the comma.
angular.module('yourModule', []).filter('commaFilter', function() {
return function(input) {
return input ? input + 'j' : '';
};
});
used like
{{ streetAddress | commaFilter }}
Do it in the controller:
var fullAddress = [],
components = [fullAddress,apartment,floor,city,state,zipCode];
// ES5
components.forEach(function(item) {
if (item) {
fullAddress.push(item);
}
});
// pre-ES5
for (i=0;i<components.length;i++) {
if (components[i]) {
fullAddress.push(components[i]);
}
}
$scope.fullAddress = fullAddress.join(",");
And then your template is easily:
<span>{{fullAddress}}</span>

angularjs search and ignore spanish characters

I'm adding a simple sort on a page. The idea is to search products. These products are written in spanish language and has accents. For example: 'Jamón'.
Here is my code:
<div class="form-inline">
<label>Search</label>
<input type="text" ng-model="q"/>
</div>
<div ng-repeat="product in products_filtered = (category.products | filter:q | orderBy:'name')">
....
</div>
The only problem i have is that you have to type in "Jamón" in order to find the product "Jamón". What I want is to be more flexible, if the user types in "Jamon", the results must include "Jamón".
How can i search with angular filters and forget about accents? Any Idea?
Thanks in advance.
You'll need to create a filter function (or a full filter). This is the simplest thing that could possibly work:
HTML
<div ng-app ng-controller="Ctrl">
<input type="text" ng-model="search">
<ul>
<li ng-repeat="name in names | filter:ignoreAccents">{{ name }}</li>
</ul>
</div>
Javascript
function Ctrl($scope) {
function removeAccents(value) {
return value
.replace(/á/g, 'a')
.replace(/é/g, 'e')
.replace(/í/g, 'i')
.replace(/ó/g, 'o')
.replace(/ú/g, 'u');
}
$scope.ignoreAccents = function(item) {
if (!$scope.search) return true;
var text = removeAccents(item.toLowerCase())
var search = removeAccents($scope.search.toLowerCase());
return text.indexOf(search) > -1;
};
$scope.names = ['Jamón', 'Andrés', 'Cristián', 'Fernán', 'Raúl', 'Agustín'];
};
jsFiddle here.
Please notice that this only works for arrays of strings. If you want to filter a list of objects (and search in every property of every object, like Angular does) you'll have to enhance the filter function. I think this example should get you started.
JavaScript has a neat function for this, called localeCompare, where you can specify sensitivity = base, an option that makes it treat á and a as equivalent, but it is not widely supported. I think your only option is to create a filter function wherein you normalise both strings manually (replacing ó with o and so on) and compare the results.
Here is a slightly enhanced version (not counting the extra foreign characters it searches for, of course) of the above. I put it straight into my controller and it allowed me to search all the data that I wanted to search. Thanks Bernardo for your input into this!
Hope it helps somebody out.
function removeAccents(value) {
return value
.replace(/á/g, 'a')
.replace(/â/g, 'a')
.replace(/é/g, 'e')
.replace(/è/g, 'e')
.replace(/ê/g, 'e')
.replace(/í/g, 'i')
.replace(/ï/g, 'i')
.replace(/ì/g, 'i')
.replace(/ó/g, 'o')
.replace(/ô/g, 'o')
.replace(/ú/g, 'u')
.replace(/ü/g, 'u')
.replace(/ç/g, 'c')
.replace(/ß/g, 's');
}
$scope.ignoreAccents = function (item) {
if ($scope.search) {
var search = removeAccents($scope.search).toLowerCase();
var find = removeAccents(item.name + ' ' + item.description+ ' ' + item.producer+ ' ' + item.region).toLowerCase();
return find.indexOf(search) > -1;
}
return true;
};
The method, proposed by #Michael Benford has a big disadvantage: it is a stateful filter. This practice is strongly discouraged by angular. Plus, the method doesn’t work with deep arrays and objects.
I prefer to extend the standard angular filter:
HTML:
<div ng-app ng-controller="Ctrl">
<input type="text" ng-model="search">
<ul>
<li ng-repeat="person in people | filter: search : searchFn">{{ person.names }} {{ person.surnames }}</li>
</ul>
</div>
Javascript:
function Ctrl($scope) {
$scope.people = [{names: 'Jose', surnames: 'Benítez'}, {names: 'José María', surnames: 'Núñez Rico'}, {names: 'Rodrigo', surnames: 'Núñez'}];
$scope.searchFn = function(actual, expected) {
if (angular.isObject(actual)) return false;
function removeAccents(value) {
return value.toString().replace(/á/g, 'a').replace(/é/g, 'e').replace(/í/g, 'i').replace(/ó/g, 'o').replace(/ú/g, 'u').replace(/ñ/g, 'n');
}
actual = removeAccents(angular.lowercase('' + actual));
expected = removeAccents(angular.lowercase('' + expected));
return actual.indexOf(expected) !== -1;
}
}
Check here for solutions to filter a list of objects and search keywords in every property of every object. It also maintain angular like search and also search with/ without accent characters.
Following is the filter definition:-
// ignore accents filter
$scope.ignoreAccents = function (item) {
if (!$scope.search) return true;
var objects = [];
var jsonstr = JSON.stringify(item);
var parsejson = JSON.parse(jsonstr);
var searchterm = $scope.search.replace(/[!#$%&'()*+,-./:;?#[\\\]_`{|}~]/g, ''); // skip replace if not required (it removes special characters)
objects = getNoOfResults(parsejson, searchterm);
return objects.length > 0;
};

Resources