Fork me on GitHub
Note: The Socially 2 tutorial is no longer maintained. Instead, we have a new, better and comprehensive tutorial, integrated with our WhatsApp clone tutorial: WhatsApp clone tutorial with Ionic 2, Angular 2 and Meteor (we just moved all features and steps, and implemented them better!)

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 we are going to:

  • create a new component to add or remove a party
  • learn about model-driven forms and create one
  • learn how to hook up form events to component methods
  • implement adding & removing party event handlers

First, let's create a simple form with a button that will add a new party.

Component Architecture

In Angular 2, we build a tree of components with the root App component and child components stemming out of it down to the leaves.

Let's make a new component called PartiesFormComponent, and put it inside parties directory on the client-side (client/imports/app/parties).

Notice that we are placing the file inside the imports folder.
That is another Meteor special folder name that tells Meteor to load the modules inside it just when some other module is importing it.

5.1 Create PartiesForm component client/imports/app/parties/parties-form.component.ts
1
2
3
4
5
6
7
8
9
import { Component } from '@angular/core';
 
import template from './parties-form.component.html';
 
@Component({
  selector: 'parties-form',
  template
})
export class PartiesFormComponent {}

Notice that we are exporting the class PartiesFormComponent using ES6 module syntax. As a result, you'll be able to import PartiesFormComponent in any other component as follows:

import { PartiesFormComponent } from 'client/imports/app/parties/parties-form.component';

By exporting and importing different modules, you create a modular structure of your app in ES6, which is similar to the modules in other script languages like Python. This is what makes programming in ES6 really awesome since application structure comes out rigid and clear.

Let's add a template for the new component.

Add a file with the following form:

5.2 Create template of PartiesForm client/imports/app/parties/parties-form.component.html
1
2
3
4
5
6
7
8
9
10
11
12
<form>
  <label>Name</label>
  <input type="text">
 
  <label>Description</label>
  <input type="text">
 
  <label>Location</label>
  <input type="text">
  
  <button>Add</button>
</form>

We can load the new PartiesForm component on the page by placing the <parties-form> tag in the root template app.html:

5.3 Add PartiesForm to App client/imports/app/app.component.html
1
2
3
4
5
6
<div>
  <parties-form></parties-form>
  
  <ul>
    <li *ngFor="let party of parties | async">
      {{party.name}}

There is one more required step in Angular 2 to load a component - we need to declare it in the our NgModule so other Components know it existing and can use it.

We will create a new file that exports an Array of Components that needed to be declared in the NgModule:

5.4 Create index for parties with declarations client/imports/app/parties/index.ts
1
2
3
4
5
import { PartiesFormComponent } from './parties-form.component';
 
export const PARTIES_DECLARATIONS = [
  PartiesFormComponent
];

And now let's load this Array of Components into our NgModule:

5.5 Add parties declarations to AppModule client/imports/app/app.module.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { BrowserModule } from '@angular/platform-browser';
 
import { AppComponent } from './app.component';
import { PARTIES_DECLARATIONS } from './parties';
 
@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    ...PARTIES_DECLARATIONS
  ],
  bootstrap: [
    AppComponent

The ... is part of ES2016 language - it spreads the array like it was not an array, you can read more about it here.

Now we have our parties-form directive showing on our app.

Angular 2 Forms

Now let's get back to the form and make it functional.

Model-Driven Forms

In order to use features of Angular 2 for Forms - we need to import FormsModule into our NgModule, so let's do it:

5.6 Import Forms modules client/imports/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
import { AppComponent } from './app.component';
import { PARTIES_DECLARATIONS } from './parties';
 
@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [
    AppComponent,

Full documentation of FormsModule and a comprehensive tutorial is located here.

Let's construct our form model. There is a special class for this called FormBuilder.

First, we should import necessary dependencies, then build the model and its future fields with help of the FormBuilder instance:

1
2
3
4
5
 
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
 
import template from './parties-form.component.html';
 
...some lines skipped...
  selector: 'parties-form',
  template
})
export class PartiesFormComponent implements OnInit {
  addForm: FormGroup;
 
  constructor(
    private formBuilder: FormBuilder
  ) {}
 
  ngOnInit() {
    this.addForm = this.formBuilder.group({
      name: [],
      description: [],
      location: []
    });
  }
}

As you probably noticed, we used OnInit interface. It brings the ngOnInit method. It initialize the directive/component after Angular initializes the data-bound input properties. Angular will find and call methods like ngOnInit(), with or without the interfaces. Nonetheless, we strongly recommend adding interfaces to TypeScript directive classes in order to benefit from strong typing and editor tooling.

FormGroup is a set of FormControls.

Alternatively, we could write:

this.addForm = new FormGroup({
  name: new FormControl()
});

The first value provided is the initial value for the form control. For example:

this.addForm = this.formBuilder.group({
  name: ['Bob']
});

will initialize name to Bob value.

We can use addForm.value to access current state of the model:

console.log(this.addForm.value);
> { name: '', description: '', location: ''}

We could also access the control values individually.

console.log(this.addForm.controls.name.value);
> ''

Now let's move to the template. We have to bind to formGroup and add formControlName directives to our inputs.

1
2
3
4
5
6
7
8
9
10
11
12
<form [formGroup]="addForm">
  <label>Name</label>
  <input type="text" formControlName="name">
 
  <label>Description</label>
  <input type="text" formControlName="description">
 
  <label>Location</label>
  <input type="text" formControlName="location">
  
  <button type="submit">Add</button>
</form>

By formGroup we provide an instance of the FormGroup, in our case this is the addForm.

But what about those formControlName directives? As you can see, we implemented them with values that match our addForm structure. Each formControlName binds value of a form element to the model.

Now each time the user types inside these inputs, the value of the addForm and its controls will be automatically updated.

Conversely, if addForm is changed outside of the HTML, the input values will be updated accordingly.

Since name and location are required fields in our model, let's set up validation.

In Angular2, it's less then easy, just add Validators.required as a second parameter to a required control:

1
2
3
4
5
 
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
 
import template from './parties-form.component.html';
 
...some lines skipped...
 
  ngOnInit() {
    this.addForm = this.formBuilder.group({
      name: ['', Validators.required],
      description: [],
      location: ['', Validators.required]
    });
  }
}

We can check addForm.valid property to determine if the form is valid:

console.log(this.addForm.valid)
> false

Event Handlers

(ngSubmit)

We just set up the form and synchronized it with the form model.

Let's start adding new parties to the Parties collection. Before we start, we create a new submit button and a form submit event handler.

It's worth mentioning one more great feature that appeared in Angular 2. It's possible now to define and use local variables in a template.

For example, if we were using Template-driven Forms, to add a party we would need to take the current state of the form and pass it to an event handler. We could take the form and print it inside the template:

<form #f="ngForm">
    ...
    \{{f.value}}
</form>

you'll see something like:

{name: '', description: '', location: ''}

which is exactly what we would need — the form model object.

Since we decided to use Model-driven Forms we won't use it, but I think it's worth to mention because of its simplicity and power.

Back to the tutorial!

Let's bind a submit event to the add button.

This event will trigger if the button is clicked, or if the user presses enter on the final field.

1
2
3
4
<form [formGroup]="addForm" (ngSubmit)="addParty()">
  <label>Name</label>
  <input type="text" formControlName="name">
 

In Angular 2, events are indicated by the round bracket () syntax. Here we are telling Angular to call a method addParty on submit. Let's add the addParty method to our PartiesFormComponent class.

1
2
3
4
5
6
7
8
 
23
24
25
26
27
28
29
30
31
32
33
34
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
 
import { Parties } from '../../../../both/collections/parties.collection';
 
import template from './parties-form.component.html';
 
@Component({
...some lines skipped...
      location: ['', Validators.required]
    });
  }
 
  addParty(): void {
    if (this.addForm.valid) {
      Parties.insert(this.addForm.value);
 
      this.addForm.reset();
    }
  }
}

Note: TypeScript doesn't know which controls properties are available so we have to put them in the squery brackets.

Open a different browser, fill out the form, submit and see how the party is added on both clients.

Types

In order to get a better coded application, we will use the power of TypeScript and declare our types, models and interfaces of the database objects.

First, we will get warning and errors from the TypeScript compiler, and we also get great IDE support if you uses WebStorm or VSCode.

So first, let's create a base model for our database entities, which contains the _id field:

5.14 Create CollectionObject model both/models/collection-object.model.ts
1
2
3
export interface CollectionObject {
  _id?: string;
}

And let's create a model for a single Party object:

5.15 Extend Party by CollectionObject model both/models/party.model.ts
1
2
3
4
5
6
import { CollectionObject } from './collection-object.model';
 
export interface Party extends CollectionObject {
  name: string;
  description: string;
  location: string;

We will later use those to indicate the types of our collection and objects in the UI.

(click)

Now, let's add the ability to delete parties.

Let's add an X button to each party in our party list:

5.12 Add remove button client/imports/app/app.component.html
6
7
8
9
10
11
12
      {{party.name}}
      <p>{{party.description}}</p>
      <p>{{party.location}}</p>
      <button (click)="removeParty(party)">X</button>
    </li>
  </ul>
</div>

Here again, we are binding an event to the class context and passing in the party as a parameter.

Let's go into the class and add that method.

Add the method inside the AppComponent class in app.component.ts:

5.13 Implement removeParty method client/imports/app/app.component.ts
16
17
18
19
20
21
22
23
  constructor() {
    this.parties = Parties.find({}).zone();
  }
 
  removeParty(party: Party): void {
    Parties.remove(party._id);
  }
}

The Mongo Collection Parties has a method called "remove". We search for the relevant party by its identifier, _id, and delete it.

Now try to delete a few parties. Since Meteor syncs data between clients, you can also watch them being removed from other browser clients.

Summary

In this chapter we've seen:

  • How easy it is to create a form and access its data using Angular 2's power.
  • How easy it is to save that data to the storage using Meteor's power.
  • How to declare TypeScript interfaces and models.