Data Management

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

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.

4.1 Create template for PartyAdd component imports/ui/components/partyAdd/partyAdd.html
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>
4.2 Create PartyAdd component imports/ui/components/partyAdd/partyAdd.js
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

4.3 Implement PartyAdd to the view imports/ui/components/partiesList/partiesList.html
1
2
3
4
5
<party-add></party-add>
 
<ul>
  <li ng-repeat="party in partiesList.parties">
    {{party.name}}
4.4 Add PartyAdd to PartiesList imports/ui/components/partiesList/partiesList.js
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.

ng-model

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:

4.5 Add ng-model to the form inputs imports/ui/components/partyAdd/partyAdd.html
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.

ng-click

Now let's bind a click event to the add button with Angular 1's ng-click directive.

4.6 Add submit method to the button imports/ui/components/partyAdd/partyAdd.html
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:

4.8 Move Parties to imports imports/api/parties.js
1
2
3
import { Mongo } from 'meteor/mongo';
 
export const Parties = new Mongo.Collection('parties');
4.10 Import new module with Parties collection server/main.js
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

4.11 Import new module with Parties to PartiesList imports/ui/components/partiesList/partiesList.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!

4.13 Create view for PartyRemove component imports/ui/components/partyRemove/partyRemove.html
1
<button ng-click="partyRemove.remove()">X</button>
4.14 Create PartyRemove component imports/ui/components/partyRemove/partyRemove.js
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

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:

4.16 Remove party from collection imports/ui/components/partyRemove/partyRemove.js
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>

Summary

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.

Testing

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:

4.20 Add `test package.json
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);
      });
    });
  });
});