Now that we have full data binding from server to client, let's interact with the data and see the updates in action.
In this chapter you will add the ability to insert a new party and delete an existing one from the UI.
First, let's create a simple PartyAdd
component with a form and a button that will add a new party, we will add it above the list, inside the PartiesList
Component view.
1
2
3
4
5
6
7
8
9
10
11
<form>
<label>
Party Name:
</label>
<input type="text"/>
<label>
Description:
</label>
<input type="text"/>
<button>Add Party!</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import template from './partyAdd.html';
class PartyAdd {}
const name = 'partyAdd';
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
controllerAs: name,
controller: PartyAdd
});
Add PartyAdd
to the PartiesList
component
1
2
3
4
5
<party-add></party-add>
<ul>
<li ng-repeat="party in partiesList.parties">
{{party.name}}
2
3
4
5
6
7
8
22
23
24
25
26
27
28
29
import angularMeteor from 'angular-meteor';
import template from './partiesList.html';
import { name as PartyAdd } from '../partyAdd/partyAdd';
class PartiesList {
constructor($scope, $reactive) {
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor,
PartyAdd
]).component(name, {
template,
controllerAs: name,
Now we need to make this form functional.
First things first, let's bind the value of the inputs into a new party variable.
To do that we'll use the simple and powerful ng-model Angular 1 directive.
Add ng-model
to the form like this:
2
3
4
5
6
7
8
9
10
11
<label>
Party Name:
</label>
<input type="text" ng-model="partyAdd.party.name" />
<label>
Description:
</label>
<input type="text" ng-model="partyAdd.party.description" />
<button>Add Party!</button>
</form>
Now each time the user types inside these inputs, the value of the party
variable will be automatically updated. Conversely, if partyAdd.party
is changed outside of the HTML, the input values will be updated accordingly.
Now let's bind a click event to the add button with Angular 1's ng-click directive.
7
8
9
10
11
Description:
</label>
<input type="text" ng-model="partyAdd.party.description" />
<button ng-click="partyAdd.submit()">Add Party!</button>
</form>
ng-click
binds the click event to an expression - we just call a method that we will implement soon on the PartyAdd
!
Now let's implement the logic on the controller of the Component:
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import template from './partyAdd.html';
class PartyAdd {
constructor() {
this.party = {};
}
submit() {
console.log('submit:', this.party);
}
}
const name = 'partyAdd';
Since we're using modules, let's take care of Parties
collection:
1
2
3
import { Mongo } from 'meteor/mongo';
export const Parties = new Mongo.Collection('parties');
1
2
3
4
5
import { Meteor } from 'meteor/meteor';
import { Parties } from '../imports/api/parties';
Meteor.startup(() => {
if (Parties.find().count() === 0) {
Then, we remove all contents from collections/parties.js
2
3
4
5
6
7
8
import angularMeteor from 'angular-meteor';
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
import { name as PartyAdd } from '../partyAdd/partyAdd';
class PartiesList {
2
3
4
5
6
7
8
10
11
12
13
14
15
16
17
18
19
20
21
import angularMeteor from 'angular-meteor';
import template from './partyAdd.html';
import { Parties } from '../../../api/parties';
class PartyAdd {
constructor() {
...some lines skipped...
}
submit() {
Parties.insert(this.party);
this.reset();
}
reset() {
this.party = {};
}
}
Parties is a Mongo.Collection object, and the insert method inserts a new object to the collection and assign an id for the new object.
Meteor supports Javascript ES2015 by default so we can take advantage of that and define our
save
method as a method of PartyAdd class.
Open a different browser, click the button and see how the party is added on both clients. So simple!
Now, let's add the ability to delete parties.
First, we have to create a Component, let's call it PartyRemove
!
1
<button ng-click="partyRemove.remove()">X</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import template from './partyRemove.html';
class PartyRemove {
remove() {
console.log('remove party');
}
}
const name = 'partyRemove';
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
controllerAs: name,
controller: PartyRemove
});
Bindings are the way to use an external data in a component and to add communication between components.
There are four types of binding:
Value
@
- Passing a string value to a component.
<my-component my-attr="hello {{name}}">
angular.module('demo')
.component('myComponent', {
bindings: {
myAttr: '@'
},
controllerAs: 'myComponent',
controller: function() {
// on some action
this.showName = () => {
console.log(this.myAttr); // outputs a value of `hello {{name}}`
};
}
});
Two-way
=
- It means that you're setting up bidirectional binding between components. Any change of myAttr
variable will reflect the value of name
, and vice versa.
<my-component my-attr="name">
angular.module('demo')
.component('myComponent', {
bindings: {
myAttr: '='
},
controllerAs: 'myComponent',
controller: function() {
// on some action
this.showName = () => {
console.log(this.myAttr); // outputs name's value
};
this.changeName = () => {
this.myAttr = 'Kamil'; // also changes a value of `name` variable
};
}
});
One-way
>
- It means that you're setting up one-directional binding components. Any changes to name
will be reflected in myAttr
, but changes in myAttr
will not reflect in name
.
<my-component my-attr="name">
angular.module('demo')
.component('myComponent', {
bindings: {
myAttr: '>'
},
controllerAs: 'myComponent',
controller: function() {
// on some action
this.showName = () => {
console.log(this.myAttr); // outputs name's value
};
this.changeName = () => {
this.myAttr = 'Kamil'; // does not change a value of `name` variable
};
}
});
Expression
&
- An expression. Provides a way to execute an expression in the context of the parent component.
<my-component my-attr="setName(newName)">
angular.module('demo')
.component('myComponent', {
bindings: {
myAttr: '&'
},
controllerAs: 'myComponent',
controller: function() {
// on some action
this.submit = () => {
// execute the myAttr expression
this.myAttr({
newName: 'Donald'
});
};
}
});
You can read more about bindings on Official Angular's documentation.
Since you know how something about bindings we can continue our work!
Use one-way binding since this is available in AngularJS 1.5:
16
17
18
19
20
21
22
23
24
angularMeteor
]).component(name, {
template,
bindings: {
party: '<'
},
controllerAs: name,
controller: PartyRemove
});
Now, we can add some logic to remove() method:
2
3
4
5
6
7
8
9
10
11
12
13
14
import angularMeteor from 'angular-meteor';
import template from './partyRemove.html';
import { Parties } from '../../../api/parties';
class PartyRemove {
remove() {
if (this.party) {
Parties.remove(this.party._id);
}
}
}
It's not yet available in PartiesList
:
4
5
6
7
8
9
10
25
26
27
28
29
30
31
32
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
import { name as PartyAdd } from '../partyAdd/partyAdd';
import { name as PartyRemove } from '../partyRemove/partyRemove';
class PartiesList {
constructor($scope, $reactive) {
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor,
PartyAdd,
PartyRemove
]).component(name, {
template,
controllerAs: name,
4
5
6
7
8
9
<li ng-repeat="party in partiesList.parties">
{{party.name}}
<p>{{party.description}}</p>
<party-remove party="party"></party-remove>
</li>
</ul>
So now you've seen how easy it is to manipulate the data using Angular 1's powerful directives and sync that data with Meteor's powerful Mongo.Collection API.
One of the new features of Meteor 1.3 is support for testing. In Socially we want to use Jasmine. Let's add it to our app!
$ meteor add sanjo:jasmine
You probably want to see result of tests:
$ meteor add velocity:html-reporter
$ meteor add velocity:console-reporter
We also have to use angular-mocks
:
$ meteor npm install --save-dev angular-mocks
Now, add a script to run unit-tests:
2
3
4
5
6
7
8
9
"name": "socially",
"private": true,
"scripts": {
"start": "meteor run",
"test:watch": "meteor test --driver-package sanjo:jasmine"
},
"dependencies": {
"angular": "^1.5.3",
Use this command to run tests:
$ meteor npm run test:watch
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
import { name as PartyAdd } from '../partyAdd';
import { Parties } from '../../../../api/parties';
import 'angular-mocks';
describe('PartyAdd', () => {
beforeEach(() => {
window.module(PartyAdd);
});
describe('controller', () => {
let controller;
const party = {
name: 'Foo',
description: 'Birthday of Foo'
};
beforeEach(() => {
inject(($rootScope, $componentController) => {
controller = $componentController(PartyAdd, {
$scope: $rootScope.$new(true)
});
});
});
describe('reset()', () => {
it('should clean up party object', () => {
controller.party = party;
controller.reset();
expect(controller.party).toEqual({});
});
});
describe('submit()', () => {
beforeEach(() => {
spyOn(Parties, 'insert');
spyOn(controller, 'reset').and.callThrough();
controller.party = party;
controller.submit();
});
it('should insert a new party', () => {
expect(Parties.insert).toHaveBeenCalledWith(party);
});
it('should call reset()', () => {
expect(controller.reset).toHaveBeenCalled();
});
});
});
});
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
import { name as PartyRemove } from '../partyRemove';
import { Parties } from '../../../../api/parties';
import 'angular-mocks';
describe('PartyRemove', () => {
beforeEach(() => {
window.module(PartyRemove);
});
describe('controller', () => {
let controller;
const party = {
_id: 'partyId'
};
beforeEach(() => {
inject(($rootScope, $componentController) => {
controller = $componentController(PartyRemove, {
$scope: $rootScope.$new(true)
}, {
party
});
});
});
describe('remove()', () => {
beforeEach(() => {
spyOn(Parties, 'remove');
controller.remove();
});
it('should remove a party', () => {
expect(Parties.remove).toHaveBeenCalledWith(party._id);
});
});
});
});