AngularJS Pipes

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

Our next mission is to invite users to private parties.

We have subscribed to list of all users, but we can't invite everyone. We can't invite the owner of the party and we can't invite users who are already invited, so why not filter them out of the view?

To do so we will use the powerful filter feature of Angular 1.

Filters can work on array as well as single values. We can aggregate any number of filters on top of each other.

Here is the list of all of Angular 1 built-in filters: https://docs.angularjs.org/api/ng/filter

And here is a 3rd party library with many more filters: angular-filter

Now let's create a custom filter that will filter out users that are the owners of a certain party and that are already invited to it.

Create a new folder named filters under the imports/ui folder.

Under that folder create a new file named uninvitedFilter.js and place that code inside:

13.1 Create UninvitedFilter imports/ui/filters/uninvitedFilter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import angular from 'angular';
 
const name = 'uninvitedFilter';
 
function UninvitedFilter(users, party) {
  if (!party) {
    return false;
  }
 
  return users.filter((user) => {
    // if not the owner and not invited
    return user._id !== party.owner && (party.invited || []).indexOf(user._id) === -1;
  });
}
 
// create a module
export default angular.module(name, [])
  .filter(name, () => {
    return UninvitedFilter;
  });
  • First we create a module named uninvitedFilter
  • Then we define a filter to the module with the same name
  • Filters always get at least one parameter and the first parameter is always the object or array that we are filtering (like the parties in the previous example) Here we are filtering the users array, so that's the first parameter
  • The second parameter is the party we want to check
  • The first if statement is to make sure we passed the initializing phase of the party and it's not undefined

At this point we need to return the filtered array.

We use filter method to remove each user that neither is the party's owner nor hasn't been invited.

To make our lives easier, we can just use underscore package.

$ meteor npm install --save underscore
1
2
3
4
5
 
10
11
12
13
14
15
16
import angular from 'angular';
import _ from 'underscore';
 
const name = 'uninvitedFilter';
 
...some lines skipped...
 
  return users.filter((user) => {
    // if not the owner and not invited
    return user._id !== party.owner && !_.contains(party.invited, user._id);
  });
}
 

So now let's use our new filter.

We will create a component to display list of uninvited users. Let's call it PartyUninvited.

First, we need a template. Use already exist one view from PartyDetails and move it to a separate file:

13.4 Move list of users to separate component imports/ui/components/partyUninvited/partyUninvited.html
1
2
3
4
5
6
<ul>
  Users:
  <li ng-repeat="user in partyUninvited.users">
    <div>{{ user.emails[0].address }}</div>
  </li>
</ul>

Then, create the actual component:

13.5 Create PartyUninvited component imports/ui/components/partyUninvited/partyUninvited.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import { Meteor } from 'meteor/meteor';
 
import template from './partyUninvited.html';
 
class PartyUninvited {
  constructor($scope) {
    'ngInject';
 
    $scope.viewModel(this);
 
    this.helpers({
      users() {
        return Meteor.users.find({});
      }
    });
  }
}
 
const name = 'partyUninvited';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  controllerAs: name,
  bindings: {
    party: '<'
  },
  controller: PartyUninvited
});

PartyUninvited has a one-way binding called party. Without a party we can't say who hasn't been invited!

Since we have users helper we have also add UninvitedFilter:

4
5
6
7
8
9
10
 
24
25
26
27
28
29
30
31
import { Meteor } from 'meteor/meteor';
 
import template from './partyUninvited.html';
import { name as UninvitedFilter } from '../../filters/uninvitedFilter';
 
class PartyUninvited {
  constructor($scope) {
...some lines skipped...
 
// create a module
export default angular.module(name, [
  angularMeteor,
  UninvitedFilter
]).component(name, {
  template,
  controllerAs: name,

Let's use the filter:

1
2
3
4
5
6
<ul>
  Users to invite:
  <li ng-repeat="user in partyUninvited.users | uninvitedFilter:partyUninvited.party">
    <div>{{ user.emails[0].address }}</div>
  </li>
</ul>

And add it to the PartyDetails component

13.8 Implement component in PartyDetails view imports/ui/components/partyDetails/partyDetails.html
8
9
10
11
 
<button ui-sref="parties">Back</button>
 
<party-uninvited party="partyDetails.party"></party-uninvited>
6
7
8
9
10
11
12
 
55
56
57
58
59
60
61
62
 
import template from './partyDetails.html';
import { Parties } from '../../../api/parties';
import { name as PartyUninvited } from '../partyUninvited/partyUninvited';
 
class PartyDetails {
  constructor($stateParams, $scope, $reactive) {
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter,
  PartyUninvited
]).component(name, {
  template,
  controllerAs: name,

Run the app and see the users in each party.

We still don't have invites but you can see that the filter already filters the party owners out of the list.

But some of the users don't have emails (maybe some of them may have signed in with Facebook). In that case we want to display their name and not the empty email field.

But it's only in the display so its perfect for a filter.

So let's create another custom filter DisplayNameFilter.

Create a new file under the filters folder named displayNameFiler.js and place that code inside:

13.10 Create DisplayNameFilter imports/ui/filters/displayNameFilter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import angular from 'angular';
 
const name = 'displayNameFilter';
 
function DisplayNameFilter(user) {
  if (!user) {
    return '';
  }
 
  if (user.profile && user.profile.name) {
    return user.profile.name;
  }
 
  if (user.emails) {
    return user.emails[0].address;
  }
 
  return user;
}
 
// create a module
export default angular.module(name, [])
  .filter(name, () => {
    return DisplayNameFilter;
  });

Pretty simple logic but it's so much nicer to put it here and make the HTML shorter and more readable.

AngularJS can also display the return value of a function in the HTML.

To demonstrate let's use DisplayNameFilter in PartyUninvited:

5
6
7
8
9
10
11
 
26
27
28
29
30
31
32
33
 
import template from './partyUninvited.html';
import { name as UninvitedFilter } from '../../filters/uninvitedFilter';
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
 
class PartyUninvited {
  constructor($scope) {
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  UninvitedFilter,
  DisplayNameFilter
]).component(name, {
  template,
  controllerAs: name,
1
2
3
4
5
6
<ul>
  Users to invite:
  <li ng-repeat="user in partyUninvited.users | uninvitedFilter:partyUninvited.party">
    <div>{{ user | displayNameFilter }}</div>
  </li>
</ul>

We have now list of uninvited users but we don't have an information about owner of each party.

Let's create PartyCreator component:

13.13 Create template for PartyCreator component imports/ui/components/partyCreator/partyCreator.html
1
2
3
4
5
<p>
  <small>
    Posted by {{ partyCreator.creator | displayNameFilter }}
  </small>
</p>
13.14 Create PartyCreator component imports/ui/components/partyCreator/partyCreator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import { Meteor } from 'meteor/meteor';
 
import template from './partyCreator.html';
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
 
/**
 * PartyCreator component
 */
class PartyCreator {
  constructor($scope) {
    'ngInject';
 
    $scope.viewModel(this);
 
    this.subscribe('users');
 
    this.helpers({
      creator() {
        if (!this.party) {
          return '';
        }
 
        const owner = this.party.owner;
 
        if (Meteor.userId() !== null && owner === Meteor.userId()) {
          return 'me';
        }
 
        return Meteor.users.findOne(owner) || 'nobody';
      }
    });
  }
}
 
const name = 'partyCreator';
 
// create a module
export default angular.module(name, [
  angularMeteor,
  DisplayNameFilter
]).component(name, {
  template,
  controllerAs: name,
  bindings: {
    party: '<'
  },
  controller: PartyCreator
});

We created a creator helper that tell the viewer who the owner is. Now we have to implement it in the PartiesList component:

11
12
13
14
15
16
17
    </a>
    <p>{{party.description}}</p>
    <party-remove party="party"></party-remove>
    <party-creator party="party"></party-creator>
  </li>
</ul>
 
10
11
12
13
14
15
16
 
62
63
64
65
66
67
68
69
import { name as PartiesSort } from '../partiesSort/partiesSort';
import { name as PartyAdd } from '../partyAdd/partyAdd';
import { name as PartyRemove } from '../partyRemove/partyRemove';
import { name as PartyCreator } from '../partyCreator/partyCreator';
 
class PartiesList {
  constructor($scope, $reactive) {
...some lines skipped...
  utilsPagination,
  PartiesSort,
  PartyAdd,
  PartyRemove,
  PartyCreator
]).component(name, {
  template,
  controllerAs: name,

Summary

In this chapter we learned about Angular 1 filters and how easy they are to use and to read from the HTML.

In the next step we will learn about Meteor methods, which enables us to run custom logic in the server, beyond the Mongo API and the allow/deny methods.

Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { name as PartyCreator } from '../partyCreator';
import { Meteor } from 'meteor/meteor';
import 'angular-mocks';
 
describe('PartyCreator', () => {
  beforeEach(() => {
    window.module(PartyCreator);
  });
 
  describe('controller', () => {
    let $rootScope;
    let $componentController;
    const party = {
      _id: 'partyId'
    };
 
    beforeEach(() => {
      inject((_$rootScope_, _$componentController_) => {
        $rootScope = _$rootScope_;
        $componentController = _$componentController_;
      });
    });
 
    function component(bindings) {
      return $componentController(PartyCreator, {
        $scope: $rootScope.$new(true)
      }, bindings);
    }
 
    it('should return an empty string if there is no party', () => {
      const controller = component({
        party: undefined
      });
 
      expect(controller.creator).toEqual('');
    });
 
    it('should say `me` if logged in is the owner', () => {
      const owner = 'userId';
      // logged in
      spyOn(Meteor, 'userId').and.returnValue(owner);
      const controller = component({
        party: {
          owner
        }
      });
 
      expect(controller.creator).toEqual('me');
    });
 
    it('should say `nobody` if user does not exist', () => {
      const owner = 'userId';
      // not logged in
      spyOn(Meteor, 'userId').and.returnValue(null);
      // no user found
      spyOn(Meteor.users, 'findOne').and.returnValue(undefined);
      const controller = component({
        party: {
          owner
        }
      });
 
      expect(controller.creator).toEqual('nobody');
    });
 
    it('should return user data if user exists and it is not logged one', () => {
      const owner = 'userId';
      // not logged in
      spyOn(Meteor, 'userId').and.returnValue(null);
      // user found
      spyOn(Meteor.users, 'findOne').and.returnValue('found');
      const controller = component({
        party: {
          owner
        }
      });
 
      expect(controller.creator).toEqual('found');
    });
  });
});