Sort ng-options by translated values - angularjs

It's really easy to have select options translated with angular-translate:
<select name="languageId"
ng-options="p.id as ('LANGUAGE.'+p.id)|translate for p in Const.languages | orderBy:'name'">
But that way the options are sorted by their original key not their translated one.
Is there a way I can have that list ordered by their translated value without preparing that list ahead in the controller?

The easiest way that worked for me (angular 1.2.24 and angular-translate 2.14.0):
<select name="nationality" ng-model="person.nationality" ng-options="c.id as c.name | translate for c in Const.countries | orderBy:'name | translate' ">
The credit for this goes to the writer of this comment: https://github.com/angular-translate/angular-translate/issues/1064#issuecomment-267357441

You could supply a predicate function to orderBy and have that function return the translated value. It means you also have to pass in the $filter service to your controller because you'd need to invoke translate inside your JS code.
So, to offer some code as guidance:
// CONTROLLER
// * Don't forget to inject the $filter service
$scope.translated = function(p) {
return $filter('translate')('LANGUAGE.' + p.id);
}
And then you'd modify your orderBy expression:
... | orderBy:translated
I realise the suggestion seems somewhat convoluted because one translation attempt occurs within ng-options and then another in orderBy, but it should sort the select options as you'd expect.

I had the same problem and I recommend using an own filter, because you don't want to mess up your controller! This is my current solution:
/**
* orderByTranslated Filter
* Sort ng-options or ng-repeat by translated values
* #example
* ng-options="country as ('countries.'+country | translate) for country in countries | orderByTranslated:'countries.'"
* #param {Array} array
* #param {String} i18nKeyPrefix
* #param {String} objKey
* #return {Array}
*/
app.filter('orderByTranslated', ['$translate', '$filter', function($translate, $filter) {
return function(array, i18nKeyPrefix, objKey) {
var result = [];
var translated = [];
angular.forEach(array, function(value) {
var i18nKeySuffix = objKey ? value[objKey] : value;
translated.push({
key: value,
label: $translate.instant(i18nKeyPrefix + i18nKeySuffix)
});
});
angular.forEach($filter('orderBy')(translated, 'label'), function(sortedObject) {
result.push(sortedObject.key);
});
return result;
};
}]);
Notice you need to pass your i18n prefix 'LANGUAGE.' and I saw your using an object instead of a simple string array so you can use it like this:
<select name="languageId"
ng-options="p.id as ('LANGUAGE.'+p.id)|translate for p in Const.languages | orderByTranslated:'LANGUAGE.':'id'">

I know it's an old question, but stumbled upon this today and here's how in this case you could solve it (only works for Array though):
<select name="languageId"
ng-options="p.id as name=('LANGUAGE.'+p.id)|translate for p in Const.languages | orderBy:'name'">
The trick is to assign the translation and use this assignment to sort on.

Related

AngularJS v2 is not a function?

I have a very simple piece of code to run a dynamic sortBy over my array. I am using a select with ng-model to return the correct key by which to sort. However, I can change the select once, and the orderBy works. But once I do it again, I get a very strange error
Controller
//change task sort
$scope.changeOrder = 'task_date';
$scope.changeOrder = (filterTask) => {
if (filterTask == "due") {
$scope.changeOrder = 'task_date';
} else if (filterTask == "imp") {
$scope.changeOrder = 'task_importence';
}
};
Template
<select ng-change=changeOrder(filterTask) ng-model="filterTask">
<option value="due">Due First</option>
<option value="imp">Importance</option>
</select>
<task-item ng-repeat="task in $ctrl.user.task | orderBy : changeOrder"></task-item>
Here is the error - There is nothing called "v2" in my system
Welcome to the untyped world that is JavaScript.
Your error is actually quite apparent: $scope.changeOrder becomes a function and a standard variable. Once you select a value in your select drop-down, it ceases to be a function and reverts to a standard variable. Then, you can no longer call it.
You would be wise to split this up into two variables instead. I'd recommend using $scope.orderState and $scope.changeOrder, where orderState just holds the strings and changeOrder is your function.
I think the problem is that both of your $scope variables have the same name. You try to assign a function and a value to $scope.changeOrder. Try splitting it up into two variables

ng-repeat order by for specific values on a property

I am looking to order my list by a number of specific values on one property, and then some addtional properties once that is done.
The code I have is:
<tr ng-repeat-start="subcontractor in trade.SubContractors | orderBy:['Status':'PREF','Status':'APPR','Status':'LOGD','Status':'NIU','-AverageScores.AverageScore','-ProjectCount','SubcontractorName']">
Where the important bit (and the bit I can't get working) is:
'Status':'PREF','Status':'APPR','Status':'LOGD','Status':'NIU'
Is there a way of doing this in angular?
I would instead implement a handler to process data. Process subcontractors before it's set, running each element through a handler and assigning "sortValue" property to each one.
Then simply call orderBy using the sortValue property. This way you would decouple sorting data from displaying data. Though don't use a filter to do so, as it would be quite expensive resource-vise.
Something like
var statusSorting = ['PREF','APPR','LOGD','NIU'];
function sortContractors(models) {
var processed = [];
angular.forEach(models, function(model){
// logic to assign sortValue
var statusIndex = statusSorting.indexOf(model.status);
model.sortValue = statusIndex + 1;
});
return processed;
}
api.getData()
.then(function(data){
$scope.models = sortContractors(data);
});
// template
<tr ng-repeat="model in models | orderBy:'sortValue'">
You can then control priority by changing status position in the array and ordering desc/asc.
Alternately:
orderBy multiple fields in Angular
<tr ng-repeat="model in models | orderBy:['sortValue', 'contractorName']">
Your answer had a few non-required parts so I've tweaked it to give me the following:
function getStatusSortOrder(subcontractors) {
var statusSorting = ['PREF', 'APPR', 'LOGD', 'NIU'];
angular.forEach(subcontractors, function (model) {
statusIndex = statusSorting.indexOf(model.Status);
model.StatusSortValue = statusIndex;
});
}

Angular ng repeat filter inside ng repeat

I've been trying to make a list of geozones, with a select of taxes each (not all taxes apply to all geozones).
So I did a ng-repeat for the geozones, and inside each of them a ng-repeat with all taxes. Problem is I don't know how to send the id of the geozone being filtered at the moment. This is the code right now:
<md-option ng-repeat="tax in taxClasses | filter: filterTax" value="{{ '{{ tax.id }}' }}">{{ '{{ tax.name }}' }}</md-option>
and the JS:
$scope.filterTax = function(tax, n){
angular.forEach(tax.geoZone , function(geo){
if(geo === $scope.prices[n].geozoneId){
return true;
}
});
return false;
};
Need n to be the index of the geozone, or something of the sort. Thanks in advance!
Your idea is not that far off, but using filter: is not even necessary, as the pipe | is already a filter command:
ng-repeat="<var> in <array> | <filterFunction>:<...arguments>"
Thus you can create a filter (see https://docs.angularjs.org/guide/filter for details on that)
ng-repeat="tax in taxClasses | filterTax: <geozoneIndex>"
The value form the collection will be passed as the first argument of your filterTax function. Any further argument is passed separated by a colon :.
When you use this, you have to propagate a filter method like this:
app.module(...).filter('filterTax', function(/* injected services */) {
return function (/* arguments */ input, geozoneIndex) {
// function body
}
});
Alternatively use a filter function from your scope:
// template
ng-repeat="tax in filterTaxes(taxClasses, <geozoneIndex>)"
// script
$scope.filterTaxes = function(taxClasses, geozoneIndex) {
// do the whole filtering, but return an array of matches
return taxClasses.filter(function(taxClass) {
return /* in or out code */;
});
};
This means your geozoneIndex may be either a fixed value or being pulled from a variable, that's available in that scope.
Just be aware that your filterTax function will be called a lot, so if your page is getting slow you might want to consider optimizing that filtering.

AngularJS 1.5 ngOptions Comparison for Displaying a Plain Array

Can anyone tell me why the model option in the first example is selected, and the second one does not for a plain array:
// Plain old array
vm.owner = ['example', 'example2', 'example3', ...];
Where the model vm.model.address.owner = 2;
// EXAMPLE 1 - Works
// Force index to be a number: using id*1 instead of it being a string:
// and the option ('example3') is selected based on the model value of 2
// indicating the index
<select id="owner"
name="owner"
placeholder="Residential Status"
ng-model="vm.model.address.owner"
ng-required="true"
ng-options="id*1 as owner for (id, owner) in vm.owner">
<option value="">Select Value</option>
</select>
Attempting to not use a hack and using track by instead index 2 is not selected even though the value is still set in the model.
// EXAMPLE 2 - Doesn't Work
// Markup doesn't show value to be a string: using track by, but the
// option set in the model doesn't cause the option to be selected it
// remains as the default with a value of ''
<select id="owner"
name="owner"
placeholder="Residential Status"
ng-model="vm.model.address.owner"
ng-required="true"
ng-options="owner for (id, owner) in vm.owner track by id">
<option value="">Select Value</option>
</select>
I find ngOptions to be super confusing so any explanation or solution for example 2 since it is cleaner and not a hack would be great.
Didn't find a solution using track by, but the AngularJS docs for Select had a solution using a parser and formatter so I could get away from using the hack in the question. I tweaked it a bit so if the key was a string it will leave it alone, otherwise it converts it, and this seems to work. Any criticisms or issues that I don't see please comment, otherwise hope this helps someone.
(function () {
'use strict';
/**
* Binds a select field to a non-string value via ngModel parsing and formatting,
* which simply uses these pipelines to convert the string value.
* #constructor
* #ngInject
* ---
* NOTE: In general matches between a model and an option is evaluated by strict
* comparison of the model value against the value of the available options.
* Setting the option value with the option's "value" attribute the value
* will always be a "string", which means that the model value must also
* be a string, otherwise the select directive cannot match them
* reliably.
*/
function selectConvertKey(_) {
return {
require: 'ngModel',
link: function ($scope, $element, $attrs, $ctrl) {
var ngModelCtrl = $ctrl;
// Do nothing if no ng-model
if (!ngModelCtrl) {
return;
}
// ---
// PRIVATE METHODS.
// ---
/**
* Convert the key to a number if the key is a number.
* #param key
* #returns {Number}
* ---
* NOTE: Using Number() instead of parseInt() means that a string
* composed of letters and numbers, and start with a number will
* not be converted.
*/
function selectConvertKeyParser(key) {
var keyAsNumber = Number(key);
// Check if the key is not a number
if(_.isNaN(keyAsNumber)) {
return key;
}
return keyAsNumber;
}
/**
* Convert the key to a string.
* #param key
* #returns {string}
*/
function selectConvertKeyFormatter(key) {
return '' + key;
}
// ---
// MODEL PROPERTIES.
// ---
/**
* Formatters used to control how the model changes are formatted
* in the view, also known as model-to-view conversion.
* ---
* NOTE: Formatters are not invoked when the model is changed
* in the view. They are only triggered if the model changes
* in code. So you could type forever into the input, and
* the formatter would never be invoked.
*/
ngModelCtrl.$formatters.push(selectConvertKeyFormatter);
/**
* Parsers used to control how the view changes read from the
* DOM are sanitized/formatted prior to saving them to the
* model, and updating the view if required.
*/
ngModelCtrl.$parsers.push(selectConvertKeyParser);
}
};
}
selectConvertKey.$inject = [
'_'
];
angular
.module('app')
.directive('selectConvertKey', selectConvertKey);
})();
Yes, the problem appears to be that the select is being bound to a non-string value. If you do the following, it would work:
//controller
vm.model.address.owner = "2"
//html
ng-options="id as owner for (id, owner) in vm.owner"
See Angularjs ng-options using number for model does not select initial value.
Also, if you want to leave the model value as a number (2, not "2") you can try this:
ng-options="vm.owner.indexOf(owner) as owner for (id, owner) in vm.owner"
However, that may not be any less "hackish" than your working first example:
ng-options="id*1 as owner for (id, owner) in vm.owner">
See the first answer at AngularJS ng-option get index.

Angularjs get length of returned filter in ng-repeat

I'm using ng-repeat to create a list of entires and using a filter.
Is it possible to get the number of entries returned, like a length
data-ng-repeat="entry in entries | filter: { country_code : countryCode }"
It is little bit tricky, use ng-init:
ng-init="filter_len = (entries | filter: { country_code : countryCode }).length"
Example: http://jsfiddle.net/KJ3Nx/
As you know filter responsible to "filter" the input list and it returns filtered list where objects have the same structure. Otherwise you get digest cycle reentering that causes Exceptions (aka > 10 cycles).
1st way
Get length after filtering:
<pre>{{(entries| myfilter:types).length }}</pre>
See Example
2nd way
Use custom filter and get length from there.
iApp.filter('myfilter', function() {
return function( entries) {
var filtered = [];
angular.forEach(entries, function(entry) {
filtered.push(entry);
});
// here fetch list length
return filtered;
};
});
I suppose the following code will work for you:
<p>Filtered number: {{(entries|filter:{country_code:countryCode}).length}}</p>
<p>Total number: {{entries.length}}</p>

Resources