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!)

Angular2-Material & Custom Auth

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

In this chapter we will add angular2-material to our project, and update some style and layout in the project.

Angular2-material documentation of each component can be found here.

Removing Bootstrap 4

First, let's remove our previous framework (Bootstrap) by running:

$ meteor npm uninstall --save bootstrap

And let's remove the import from the main.sass file:

19.2 Remove bootstrap scss client/main.scss
1
2
3
@import "./imports/app/colors.scss";
 
html, body {

Adding angular2-material

Now we need to add angular2-material to our project - so let's do that.

Run the following command in your Terminal:

$ meteor npm install @angular/[email protected]

Now let's load the module into our NgModule:

19.4 Imported the angular2-material modules client/imports/app/app.module.ts
10
11
12
13
14
15
16
 
22
23
24
25
26
27
28
29
import { routes, ROUTES_PROVIDERS } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
import { SHARED_DECLARATIONS } from './shared';
import { MaterialModule } from "@angular/material";
 
@NgModule({
  imports: [
...some lines skipped...
    Ng2PaginationModule,
    AgmCoreModule.forRoot({
      apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA'
    }),
    MaterialModule.forRoot()
  ],
  declarations: [
    AppComponent,

Like we did in the previous chapter - let's take care of the navigation bar first.

We will use directives and components from Angular2-Material - such as md-toolbar.

Let's use it in the app component's template:

19.5 Change the nav bar and the layout client/imports/app/app.component.html
1
2
3
4
5
6
<md-toolbar color="primary">
  <a routerLink="/" class="toolbar-title">Socially2</a>
  <span class="fill-remaining-space"></span>
  <login-buttons></login-buttons>
</md-toolbar>
<router-outlet></router-outlet>

And let's create a stylesheet file for the AppComponent:

19.6 Added app component style file client/imports/app/app.component.scss
1
2
3
4
5
6
7
8
.toolbar-title {
  text-decoration: none;
  color: white;
}
 
md-toolbar {
  box-shadow: 0 2px 5px 0 rgba(0,0,0,0.26);
}

And import it into our Component:

19.7 Import app component style client/imports/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/core';
 
import template from './app.component.html';
import style from './app.component.scss';
 
@Component({
  selector: 'app',
  template,
  styles: [ style ]
})
export class AppComponent {}

And let's add .fill-remaining-space CSS class we used, and let's create a Angular 2 Material theme with the colors we like (full documentation about themes is here)

19.8 Added CSS and material theme definition client/main.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@import '../node_modules/@angular/material/core/theming/all-theme';
 
@include md-core();
$app-primary: md-palette($md-light-blue, 500, 100, 700);
$app-accent:  md-palette($md-pink, A200, A100, A400);
$app-warn:    md-palette($md-red);
$app-theme: md-light-theme($app-primary, $app-accent, $app-warn);
@include angular-material-theme($app-theme);
 
.fill-remaining-space {
  flex: 1 1 auto;
}
 
body {
  background-color: #f8f8f8;
  font-family: 'Muli', sans-serif;
  padding: 0;
  margin: 0;
}
 
.sebm-google-map-container {
  width: 450px;
  height: 450px;
}

PartiesForm component

Let's replace the label and the input with simply the md-input and md-checkbox and make the button look material:

19.9 Update the view of the parties form client/imports/app/parties/parties-form.component.html
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
<div class="form-container">
  <div class="container-background">
    <div class="form-content">
      <div class="form-center">
        <h1>Your party is missing?</h1>
        <h2>Add it now! ></h2>
      </div>
      <div class="form-center">
        <form *ngIf="user" [formGroup]="addForm" (ngSubmit)="addParty()">
          <div style="display: table-row">
            <div class="form-inputs">
              <md-input dividerColor="accent" formControlName="name" placeholder="Party name"></md-input>
              <br/>
              <md-input dividerColor="accent" formControlName="description" placeholder="Description"></md-input>
              <br/>
              <md-input dividerColor="accent" formControlName="location" placeholder="Location name"></md-input>
              <br/>
              <md-checkbox formControlName="public">Public party?</md-checkbox>
              <br/><br/>
              <button color="accent" md-raised-button type="submit">Add my party!</button>
            </div>
            <div class="form-extras">
              <sebm-google-map class="new-party-map"
                               [latitude]="newPartyPosition.lat"
                               [longitude]="newPartyPosition.lng"
                               [zoom]="8"
                               (mapClick)="mapClicked($event)">
                <sebm-google-map-marker
                  [latitude]="newPartyPosition.lat"
                  [longitude]="newPartyPosition.lng">
                </sebm-google-map-marker>
              </sebm-google-map>
            </div>
          </div>
        </form>
        <div *ngIf="!user">
          Please login in order to create new parties!
        </div>
      </div>
    </div>
  </div>
</div>

We use the mdInput component which is a wrapper for regular HTML input with style and cool layout.

Now let's add CSS styles:

19.10 Added styles for parties form client/imports/app/parties/parties-form.component.scss
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
.form-container {
  position: relative;
  display: inline-block;
  overflow-y: auto;
  overflow-x: hidden;
  flex-grow: 1;
  z-index: 1;
  width: 100%;
  color: white;
 
  .container-background {
    background: linear-gradient(rgb(0,121,107),rgb(0,150,136));
    color: #fff;
 
    .form-content {
      background: #0277bd;
      width: 100%;
      padding: 0 !important;
      align-items: center;
      display: flex;
      flex-flow: row wrap;
      margin: 0 auto;
 
      form {
        width: 100%;
        display: table;
      }
 
      .form-inputs {
        display: table-cell;
        width: 60%;
        vertical-align: top;
        text-align: center;
        margin-top: 20px;
      }
 
      .form-extras {
        display: table-cell;
        width: 40%;
        vertical-align: top;
 
        .new-party-map {
          width: 100% !important;
          height: 300px !important;
        }
      }
 
      .form-center {
        width: 50%;
        text-align: center;
      }
    }
  }
}

Now we need to make some changes in our Component's code - we will inject the user (using InjectUser), import the new stylesheet and add the ability to set the new party location using a Google map Component:

19.11 Inject user and import styles into the form client/imports/app/parties/parties-form.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
28
29
30
31
32
33
34
35
36
37
 
43
44
45
46
47
48
49
50
51
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Parties } from '../../../../both/collections/parties.collection';
import { InjectUser } from "angular2-meteor-accounts-ui";
import template from './parties-form.component.html';
import style from './parties-form.component.scss';
 
@Component({
  selector: 'parties-form',
  template,
  styles: [ style ]
})
@InjectUser("user")
export class PartiesFormComponent implements OnInit {
  addForm: FormGroup;
  newPartyPosition: {lat:number, lng: number} = {lat: 37.4292, lng: -122.1381};
 
  constructor(
    private formBuilder: FormBuilder
...some lines skipped...
    });
  }
 
  mapClicked($event) {
    this.newPartyPosition = $event.coords;
  }
 
  addParty(): void {
    if (!Meteor.userId()) {
      alert('Please log in to add a party');
...some lines skipped...
        name: this.addForm.value.name,
        description: this.addForm.value.description,
        location: {
          name: this.addForm.value.location,
          lat: this.newPartyPosition.lat,
          lng: this.newPartyPosition.lng
        },
        public: this.addForm.value.public,
        owner: Meteor.userId()

PartiesList component

PartiesForm component is done, so we can move one level higher in the component's tree. Time for the list of parties:

19.12 Updated the layout of the parties list client/imports/app/parties/parties-list.component.html
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
<parties-form></parties-form>
 
<div class="parties-list-container">
  <div class="parties-list">
    <md-card class="filter-card">
      <h3>Filter Parties</h3>
      <form>
        By Location: <md-input dividerColor="primary" type="text" #searchtext placeholder="Enter Location"></md-input>
        <button md-button (click)="search(searchtext.value)">Find</button>
        <br />
        Sort by name:
        <select class="form-control" #sort (change)="changeSortOrder(sort.value)">
          <option value="1" selected>Ascending</option>
          <option value="-1">Descending</option>
        </select>
      </form>
    </md-card>
 
    <pagination-controls class="pagination" (pageChange)="onPageChanged($event)"></pagination-controls>
 
    <md-card *ngFor="let party of parties | async" class="party-card">
      <h2 class="party-name">
        <a [routerLink]="['/party', party._id]">{{party.name}}</a>
      </h2>
      @ {{party.location.name}}
      <br />
      {{party.description}}
 
      <button class="remove-party" md-icon-button *ngIf="isOwner(party)" (click)="removeParty(party)">
        <md-icon class="md-24">X</md-icon>
      </button>
 
      <div class="rsvp-container">
        <div class="rsvps-sum">
          <div class="rsvps-amount">{{party | rsvp:'yes'}}</div>
          <div class="rsvps-title">Yes</div>
        </div>
        <div class="rsvps-sum">
          <div class="rsvps-amount">{{party | rsvp:'maybe'}}</div>
          <div class="rsvps-title">Maybe</div>
        </div>
        <div class="rsvps-sum">
          <div class="rsvps-amount">{{party | rsvp:'no'}}</div>
          <div class="rsvps-title">No</div>
        </div>
      </div>
    </md-card>
 
    <pagination-controls class="pagination" (pageChange)="onPageChanged($event)"></pagination-controls>
  </div>
  <div class="parties-map">
    <sebm-google-map
      [latitude]="0"
      [longitude]="0"
      [zoom]="1"
      class="google-map">
      <div *ngFor="let party of parties | async">
      <sebm-google-map-marker
      *ngIf="party.location.lat"
      [latitude]="party.location.lat"
      [longitude]="party.location.lng">
      </sebm-google-map-marker>
      </div>
    </sebm-google-map>
  </div>
</div>

To make it all look so much better, let's add couple of rules to css:

19.13 Updated the styles of the parties list client/imports/app/parties/parties-list.component.scss
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
.parties-list-container {
  align-items: center;
  display: flex;
  flex-flow: row wrap;
  margin: 0 auto;
  width: 100%;
  display: table;
 
  .parties-list {
    display: table-cell;
    width: 50%;
    vertical-align: top;
 
    .pagination {
      display: inline;
      text-align: center;
    }
 
    .filter-card {
      margin: 20px;
    }
 
    .party-card {
      margin: 20px;
      position: relative;
 
      .party-name > a {
        color: black;
        text-decoration: none;
      }
 
      .remove-party {
        position: absolute;
        top: 10px;
        right: 10px;
      }
 
      .rsvp-container {
        position: absolute;
        bottom: 10px;
        right: 10px;
 
        .rsvps-sum {
          display: inline-block;
          width: 50px;
          text-align: center;
 
          .rsvps-amount {
            font-size: 24px;
          }
 
          .rsvps-title {
            font-size: 13px;
            color: #aaa;
          }
        }
      }
    }
  }
 
  .parties-map {
    display: table-cell;
    width: 50%;
    vertical-align: top;
 
    .google-map {
      width: 100%;
      min-height: 600px;
    }
  }
}

PartyDetails component

We also need to update the PartyDetails component:

19.14 Update the layout of the party details client/imports/app/parties/party-details.component.html
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
<div class="party-details-container" *ngIf="party">
  <div class="row">
    <div class="party-details">
      <md-card>
        <h2>{{ party.name }}</h2>
        <form layout="column" (submit)="saveParty()">
          <label>Party Name: </label>
          <md-input [disabled]="!isOwner" [(ngModel)]="party.name" name="name"></md-input>
          <br />
          <label>Party Description: </label>
          <md-input [disabled]="!isOwner" [(ngModel)]="party.description" name="description"></md-input>
          <br />
          <label>Location name: </label>
          <md-input [disabled]="!isOwner" [(ngModel)]="party.location.name" name="location"></md-input>
          <br />
          <md-checkbox [disabled]="!isOwner" [(checked)]="party.public" name="public" aria-label="Public">
            Public party?
          </md-checkbox>
 
          <div layout="row" layout-align="left">
            <button [disabled]="!isOwner" type="submit" md-raised-button color="accent">Save</button>
            <a [routerLink]="['/']" md-raised-button class="md-raised">Back</a>
          </div>
        </form>
      </md-card>
    </div>
    <div class="party-invites">
      <md-card>
        <h2>Invitations</h2>
        <span [hidden]="!party.public">Public party, no need for invitations!</span>
        <md-list>
          <md-list-item *ngFor="let user of users | async">
            <div>{{ user | displayName }}</div>
            <button (click)="invite(user)" md-raised-button>Invite</button>
          </md-list-item>
        </md-list>
      </md-card>
    </div>
    <div class="party-map">
      <md-card>
        <h2>Party location</h2>
        <sebm-google-map
          [latitude]="lat || centerLat"
          [longitude]="lng || centerLng"
          [zoom]="8"
          (mapClick)="mapClicked($event)">
          <sebm-google-map-marker
            *ngIf="lat && lng"
            [latitude]="lat"
            [longitude]="lng">
          </sebm-google-map-marker>
        </sebm-google-map>
      </md-card>
    </div>
  </div>
</div>

And let's update the styles:

19.15 Updated the styles of the party details client/imports/app/parties/party-details.component.scss
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
.party-details-container {
  display: table;
 
  .row {
    display: table-row;
    width: 100%;
 
    .party-details, .party-invites, .party-map {
      display: table-cell;
      width: 33.3%;
      vertical-align: top;
 
      md-card {
        margin: 20px;
      }
    }
 
    .party-map {
      sebm-google-map {
        height: 300px;
        width: 100%;
      }
    }
  }
}

In this point, you can also remove the colors.scss - it's no longer in use!

Custom Authentication Components

Our next step will replace the login-buttons which is a simple and non-styled login/signup component - we will add our custom authentication component with custom style.

First, let's remove the login-buttons from the navigation bar, and replace it with custom buttons for Login / Signup / Logout.

We will also add routerLink to each button, and add logic to hide/show buttons according to the user's login state:

19.16 Replace login buttons with custom buttons client/imports/app/app.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<md-toolbar color="primary">
  <a routerLink="/" class="toolbar-title">Socially2</a>
  <span class="fill-remaining-space"></span>
  <div [hidden]="user">
    <a md-button [routerLink]="['/login']" >Login</a>
    <a md-button [routerLink]="['/signup']">Sign up</a>
  </div>
  <div [hidden]="!user">
    <span>{{ user | displayName }}</span>
    <button md-button (click)="logout()">Logout</button>
  </div>
</md-toolbar>
<router-outlet></router-outlet>

Let's use InjectUser decorator, just like we did in one of the previous chapters.

19.17 Add auth logic to the App component client/imports/app/app.component.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
import template from './app.component.html';
import style from './app.component.scss';
import {InjectUser} from "angular2-meteor-accounts-ui";
 
@Component({
  selector: 'app',
  template,
  styles: [ style ]
})
@InjectUser('user')
export class AppComponent {
  constructor() {
 
  }
 
  logout() {
    Meteor.logout();
  }
}

As you can see, we used DisplayNamePipe in the view so we have to import it.

We also implemented logout() method with Meteor.logout(). It is, like you probably guessed, to log out the current user.

Now we can move on to create three new components.

Login component

First component, is to log in user to the app.

We will need a form and the login method, so let's implement them:

19.18 Create LoginComponent client/imports/app/auth/login.component.ts
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
import {Component, OnInit, NgZone} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Meteor } from 'meteor/meteor';
 
import template from './login.component.html';
 
@Component({
  selector: 'login',
  template
})
export class LoginComponent implements OnInit {
  loginForm: FormGroup;
  error: string;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
  ngOnInit() {
    this.loginForm = this.formBuilder.group({
      email: ['', Validators.required],
      password: ['', Validators.required]
    });
 
    this.error = '';
  }
 
  login() {
    if (this.loginForm.valid) {
      Meteor.loginWithPassword(this.loginForm.value.email, this.loginForm.value.password, (err) => {
        this.zone.run(() => {
          if (err) {
            this.error = err;
          } else {
            this.router.navigate(['/']);
          }
        });
      });
    }
  }
}

Notice that we used NgZone in our constructor in order to get it from the Dependency Injection, and we used it before we update the result of the login action - we need to do this because the Meteor world does not update Angular's world, and we need to tell Angular when to update the view since the async result of the login action comes from Meteor's context.

You previously created a form by yourself so there's no need to explain the whole process once again.

About the login method.

Meteor's accounts system has a method called loginWithPassword, you can read more about it here.

We need to provide two values, a email and a password. We could get them from the form.

In the callback of Meteor.loginWithPassword's method, we have the redirection to the homepage on success and we're saving the error message if login process failed.

Let's add the view:

19.19 Create a template for LoginComponent client/imports/app/auth/login.component.html
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
<div class="md-content" layout="row" layout-align="center start" layout-fill layout-margin>
  <div layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary" color="primary">
      Sign in
    </md-toolbar>
 
    <div layout="column" layout-margin layout-padding>
      <div layout="row" layout-margin>
        <p class="md-body-2"> Sign in with your email</p>
      </div>
 
      <form [formGroup]="loginForm" #f="ngForm" (ngSubmit)="login()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="email" type="email" placeholder="Email"></md-input>
        <md-input formControlName="password" type="password" placeholder="Password"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <a md-button [routerLink]="['/recover']">Forgot password?</a>
          <button md-raised-button class="md-primary" type="submit" aria-label="login">Sign In</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>
        </md-toolbar>
      </div>
 
      <md-divider></md-divider>
 
      <div layout="row" layout-align="center">
        <a md-button [routerLink]="['/signup']">Need an account?</a>
      </div>
    </div>
  </div>
</div>

We also need to define the /login route:

19.20 Add the login route client/imports/app/app.routes.ts
3
4
5
6
7
8
9
10
11
12
13
14
 
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
import {LoginComponent} from "./auth/login.component";
 
export const routes: Route[] = [
  { path: '', component: PartiesListComponent },
  { path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] },
  { path: 'login', component: LoginComponent }
];
 
export const ROUTES_PROVIDERS = [{

And now let's create an index file for the auth files:

19.21 Create the index file for auth component client/imports/app/auth/index.ts
1
2
3
4
5
import {LoginComponent} from "./login.component";
 
export const AUTH_DECLARATIONS = [
  LoginComponent
];

And import the exposed Array into the NgModule:

19.22 Updated the NgModule imports client/imports/app/app.module.ts
11
12
13
14
15
16
17
 
29
30
31
32
33
34
35
36
import { PARTIES_DECLARATIONS } from './parties';
import { SHARED_DECLARATIONS } from './shared';
import { MaterialModule } from "@angular/material";
import { AUTH_DECLARATIONS } from "./auth/index";
 
@NgModule({
  imports: [
...some lines skipped...
  declarations: [
    AppComponent,
    ...PARTIES_DECLARATIONS,
    ...SHARED_DECLARATIONS,
    ...AUTH_DECLARATIONS
  ],
  providers: [
    ...ROUTES_PROVIDERS

Signup component

The Signup component looks pretty much the same as the Login component. We just use different method, Accounts.createUser(). Here's the link to the documentation.

19.23 Added the signup component client/imports/app/auth/signup.component.ts
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
import {Component, OnInit, NgZone} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Accounts } from 'meteor/accounts-base';
 
import template from './signup.component.html';
 
@Component({
  selector: 'signup',
  template
})
export class SignupComponent implements OnInit {
  signupForm: FormGroup;
  error: string;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
  ngOnInit() {
    this.signupForm = this.formBuilder.group({
      email: ['', Validators.required],
      password: ['', Validators.required]
    });
 
    this.error = '';
  }
 
  signup() {
    if (this.signupForm.valid) {
      Accounts.createUser({
        email: this.signupForm.value.email,
        password: this.signupForm.value.password
      }, (err) => {
        if (err) {
          this.zone.run(() => {
            this.error = err;
          });
        } else {
          this.router.navigate(['/']);
        }
      });
    }
  }
}

And the view:

19.24 Added the signup view client/imports/app/auth/signup.component.html
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
<div class="md-content" layout="row" layout-align="center start" layout-fill layout-margin>
  <div layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary" color="primary">
      Sign up
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
      <form [formGroup]="signupForm" #f="ngForm" (ngSubmit)="signup()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="email" type="email" placeholder="Email"></md-input>
        <md-input formControlName="password" type="password" placeholder="Password"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <button md-raised-button class="md-primary" type="submit" aria-label="login">Sign Up</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>
        </md-toolbar>
      </div>
 
      <md-divider></md-divider>
 
      <div layout="row" layout-align="center">
        <a md-button [routerLink]="['/login']">Already a user?</a>
      </div>
    </div>
  </div>
</div>

And add it to the index file:

19.25 Added signup component to the index file client/imports/app/auth/index.ts
1
2
3
4
5
6
7
import {LoginComponent} from "./login.component";
import {SignupComponent} from "./signup.component";
 
export const AUTH_DECLARATIONS = [
  LoginComponent,
  SignupComponent
];

And the /signup route:

19.26 Added signup route client/imports/app/app.routes.ts
4
5
6
7
8
9
10
11
12
13
14
15
16
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
import {LoginComponent} from "./auth/login.component";
import {SignupComponent} from "./auth/signup.component";
 
export const routes: Route[] = [
  { path: '', component: PartiesListComponent },
  { path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] },
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent }
];
 
export const ROUTES_PROVIDERS = [{

Recover component

This component is helfup when a user forgets his password. We'll use Accounts.forgotPassword method:

19.27 Create the recover component client/imports/app/auth/recover.component.ts
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
import {Component, OnInit, NgZone} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Accounts } from 'meteor/accounts-base';
 
import template from './recover.component.html';
 
@Component({
  selector: 'recover',
  template
})
export class RecoverComponent implements OnInit {
  recoverForm: FormGroup;
  error: string;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
  ngOnInit() {
    this.recoverForm = this.formBuilder.group({
      email: ['', Validators.required]
    });
 
    this.error = '';
  }
 
  recover() {
    if (this.recoverForm.valid) {
      Accounts.forgotPassword({
        email: this.recoverForm.value.email
      }, (err) => {
        if (err) {
          this.zone.run(() => {
            this.error = err;
          });
        } else {
          this.router.navigate(['/']);
        }
      });
    }
  }
}

Create the view:

19.28 Create the recover component view client/imports/app/auth/recover.component.html
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
<div class="md-content" layout="row" layout-align="center start" layout-fill layout-margin>
  <div layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary" color="primary">
      Recover Your Password
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
      <form [formGroup]="recoverForm" #f="ngForm" (ngSubmit)="recover()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="email" type="email" placeholder="Email"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <button md-raised-button class="md-primary" type="submit" aria-label="Recover">Recover</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>
        </md-toolbar>
      </div>
 
      <md-divider></md-divider>
 
      <div layout="row" layout-align="center">
        <a md-button [routerLink]="['/login']">Remember your password?</a>
      </div>
    </div>
  </div>
</div>

And add it to the index file:

19.29 Added the recover component to the index file client/imports/app/auth/index.ts
1
2
3
4
5
6
7
8
9
import {LoginComponent} from "./login.component";
import {SignupComponent} from "./signup.component";
import {RecoverComponent} from "./recover.component";
 
export const AUTH_DECLARATIONS = [
  LoginComponent,
  SignupComponent,
  RecoverComponent
];

And add the /reset route:

19.30 Added the recover route client/imports/app/app.routes.ts
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { PartyDetailsComponent } from './parties/party-details.component';
import {LoginComponent} from "./auth/login.component";
import {SignupComponent} from "./auth/signup.component";
import {RecoverComponent} from "./auth/recover.component";
 
export const routes: Route[] = [
  { path: '', component: PartiesListComponent },
  { path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] },
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent },
  { path: 'recover', component: RecoverComponent }
];
 
export const ROUTES_PROVIDERS = [{

That's it! we just implemented our own authentication components using Meteor's Accounts API and Angular2-Material!

Note that the recovery email won't be sent to the actual email address, since you need to configure email package to work with your email provider. you can read more about it here.

Layout and Flex

In order to use flex and layout that defined in Material, we need to add a bug CSS file into our project, that defined CSS classes for flex and layout.

You can find the CSS file content here.

So let's copy it's content and add it to client/imports/material-layout.scss.

Now let's add it to the main SCSS file imports:

19.32 Added import in the main scss file client/main.scss
22
23
24
25
26
  width: 450px;
  height: 450px;
}
 
@import "./imports/material-layout";

And let's add another CSS class missing:

19.33 Added missing CSS client/main.scss
23
24
25
26
27
28
29
30
  height: 450px;
}
 
.md-whiteframe-z2 {
  box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12);
}
 
@import "./imports/material-layout";

The import of this CSS file is temporary, and we will need to use it only because angular2-material is still in beta and not implemented all the features.

Summary

In this chapter we replaced Boostrap4 with Angular2-Material, and updated all the view and layout to match the component we got from it.

We also learnt how to use Meteor's Accounts API and how to implement authentication view and components, and how to connect them to our app.