14

In my model I have data similar to:

$scope.list = [{id:0,tags:['tag1','tag2']},{id:2,tags:['tag2']}};

I want to show a list of tags (contains unique values of 'tag1' and 'tag2') with checkboxes. Hopefully something like:

<div ng-repeat="tag in list.tags">
    <label class="checkbox">
        <input type="checkbox" ng-model="filter.tag" />
        {{tag}}
    </label>
</div>

I know how to filter the main list based on whats checked if I hard code the list, but not how to generate the list of unique tags automatically.

2 Answers 2

27

You are looking to perform three operations:

  • Get the array of tags from each item in $scope.list
  • Flatten these into a single array
  • Get the unique values from this array

You can do this with pure JavaScript, but to make things easier, I would recommend using Underscore, a library that gives you access to many functions for manipulating and inspecting arrays, objects, and so forth.

Let's start with this code:

$scope.list = [
  {id: 0, tags: ['tag1', 'tag2']},
  {id: 1, tags: ['tag2']},
  {id: 2, tags: ['tag1', 'tag3', 'tag4']},
  {id: 3, tags: ['tag3', 'tag4']}
];

Now, let's perform the first operation: get the array from the tags property for each object in $scope.list. Underscore provides the pluck method, which is just what we need.

pluck _.pluck(list, propertyName)

A convenient version of what is perhaps the most common use-case for map: extracting a list of property values.

Using pluck, we can get the following:

var tags = _.pluck($scope.list, 'tags');
// gives us [['tag1', 'tag2'], ['tag2'], ['tag1', 'tag3', 'tag4'], ['tag3', 'tag4']]

Now, we want to flatten that array.

flatten _.flatten(array, [shallow])

Flattens a nested array (the nesting can be to any depth). If you pass shallow, the array will only be flattened a single level.

tags = _.flatten(tags);
// gives us ['tag1', 'tag2', 'tag2', 'tag1', 'tag3', 'tag4', 'tag3', 'tag4']

Finally, you only want one instance of each tag.

uniq _.uniq(array, [isSorted], [iterator]) Alias: unique

Produces a duplicate-free version of the array, using === to test object equality. If you know in advance that the array is sorted, passing true for isSorted will run a much faster algorithm. If you want to compute unique items based on a transformation, pass an iterator function.

tags = _.unique(tags)
// gives us ['tag1', 'tag2', 'tag3', 'tag4']

We can combine these together with Underscore's useful chain method to chain these together. Let's create a function on the scope that returns the unique tags:

$scope.uniqueTags = function() {
  return _.chain($scope.list)
    .pluck('tags')
    .flatten()
    .unique()
    .value();
};

Since this is a function, it will always return the unique tags, no matter if we add or remove items in $scope.list after the fact.

Now you can use ng-repeat on uniqueTags to show each tag:

<div ng-repeat="tag in uniqueTags()">
  <label class="checkbox">
    <input type="checkbox" ng-model="filter.tag" />
    {{tag}}
  </label>
</div>

Here is a working jsFiddle that demonstrates this technique: http://jsfiddle.net/BinaryMuse/cqTKG/

Sign up to request clarification or add additional context in comments.

3 Comments

Perfect! And a great explanation as well!
Thanks! Happy to have helped. ^_^
What is performance like for using such an aggregation function? How often will Angular call the function?
3

Use a custom filter to get a unique set/array of tags, suitable for use with ng-repeat:

.filter('uniqueTags', function() {
    return function(list) {
        var tags = {};
        angular.forEach(list, function(obj, key) {
            angular.forEach(obj.tags, function(value) {
                tags[value] = 1;
            })
        });
        var uniqueTags = []
        for (var key in tags) {
            uniqueTags.push(key);
        }
        return uniqueTags;
    }
});

I first put the tags into an object, which automatically gives us uniqueness. Then I convert it to an array.

Use as follows:

<div ng-repeat="tag in list | uniqueTags">

Fiddle.


The following may not do what I think you probably want/expect it to do:

<input type="checkbox" ng-model="filter.tag">

This does not create $scope properties filter.tag1 and filter.tag2 on the controller scope (i.e., the scope where ng-repeat is used). Each iteration of ng-repeat creates its own child scope, so the ng-model above will create scope property filter.tag on each ng-repeat child scope, as shown in my fiddle.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.