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:
First, let's create a simple form with a button that will add a new party.
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.
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:
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
:
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 export
s an Array of Component
s that needed to be declared in the NgModule
:
1
2
3
4
5
import { PartiesFormComponent } from './parties-form.component';
export const PARTIES_DECLARATIONS = [
PartiesFormComponent
];
And now let's load this Array of Component
s into our NgModule
:
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.
Now let's get back to the form and make it functional.
In order to use features of Angular 2 for Forms - we need to import FormsModule
into our NgModule
, so let's do it:
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 FormControl
s.
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
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.
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:
1
2
3
export interface CollectionObject {
_id?: string;
}
And let's create a model for a single Party
object:
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.
Now, let's add the ability to delete parties.
Let's add an X button to each party in our party list:
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
:
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.
In this chapter we've seen: