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:
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;
});
uninvitedFilter
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:
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:
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
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:
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:
1
2
3
4
5
<p>
<small>
Posted by {{ partyCreator.creator | displayNameFilter }}
</small>
</p>
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,
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.
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');
});
});
});