Fork me on GitHub

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 step we will consider switching from Twitter Bootstrap to angular-material.

Angular-material is an Angular 1 implementation of Google's Material Design specifications. Material Design is a mobile-first design language used in many new Google's applications, especially on the Android platform.

Removing Bootstrap 4

To start, first we have to remove bootstrap from our application. Type in the console:

meteor npm uninstall bootstrap --save

We should also remove dependency from the main.js file:

18.4 Remove bootstrap from main.js client/main.js
1
2
3
4
import angular from 'angular';
 
import { Meteor } from 'meteor/meteor';
 

Installing angular-material

Now we have to add the angular-material Meteor package:

meteor npm install angular-aria angular-animate angular-material --save

We still need file with styles:

18.3 Add css client/index.html
6
7
8
9
10
11
12
  <title>Socially</title>
  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDbphq9crcdpecbseKX3Yx2LPxMRqWK-rc"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/angular-material/1.0.7/angular-material.min.css">
</head>
<body>
  <socially></socially>

Next, we want to inject the angular-material module into our Angular 1 application.

18.5 Add ngMaterial to Socially imports/ui/components/socially/socially.js
1
2
3
4
5
6
 
15
16
17
18
19
20
21
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngMaterial from 'angular-material';
import uiRouter from 'angular-ui-router';
 
import template from './socially.html';
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  ngMaterial,
  uiRouter,
  PartiesList,
  PartyDetails,

That's it, now we can use angular-material in our application layout.

Let's add Material Design Icons and Ionic Icons to Socially:

meteor add pagebakers:ionicons
meteor add planettraining:material-design-icons

We have to define the $mdIconProvider.

29
30
31
32
33
34
35
 
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  .config(config)
  .run(run);
 
function config($locationProvider, $urlRouterProvider, $qProvider, $mdIconProvider) {
  'ngInject';
 
  $locationProvider.html5Mode(true);
...some lines skipped...
  $urlRouterProvider.otherwise('/parties');
 
  $qProvider.errorOnUnhandledRejections(false);
 
  const iconPath =  '/packages/planettraining_material-design-icons/bower_components/material-design-icons/sprites/svg-sprite/';
 
  $mdIconProvider
    .iconSet('social',
      iconPath + 'svg-sprite-social.svg')
    .iconSet('action',
      iconPath + 'svg-sprite-action.svg')
    .iconSet('communication',
      iconPath + 'svg-sprite-communication.svg')
    .iconSet('content',
      iconPath + 'svg-sprite-content.svg')
    .iconSet('toggle',
      iconPath + 'svg-sprite-toggle.svg')
    .iconSet('navigation',
      iconPath + 'svg-sprite-navigation.svg')
    .iconSet('image',
      iconPath + 'svg-sprite-image.svg');
}
 
function run($rootScope, $state) {

You don't have to define all these icon sets. You just need to define those you need to use. You can see a full list of available icons here.

This is the example from PartyRemove component:

<md-icon md-svg-icon="content:ic_clear_24px"></md-icon>

In the md-svg-icon attribute we used <iconset>:<icon_name> in our case content:ic_clear_24px.

You can read more about it by clicking here

Great! So now in order get rid of all the bootstrap change we make, we need to remove some and modify some CSS and LESS.

Angular-material uses declarative syntax, i.e. directives, to utilize Material Design elements in HTML documents.

First we want to change our main component which is Socially

18.6 Clean up Socially template imports/ui/components/socially/socially.html
1
2
3
<navigation></navigation>
 
<div ui-view=""></div>

Navigation

Use md-toolbar in Navigation:

1
2
3
4
5
6
7
8
9
10
<md-toolbar>
  <div class="md-toolbar-tools">
    <h2>
      <span ui-sref="parties">
        Socially
      </span>
    </h2>
    <login-buttons></login-buttons>
  </div>
</md-toolbar>
18.8 Add space before loginButtons imports/ui/components/navigation/navigation.less
1
2
3
4
5
navigation {
  login-buttons {
    margin-left: 15px;
  }
}

PartiesList

You can see we use layout="column" in the first div tag, which tells angular-material to lay all inner layers vertically.

Element layout flex grid is very simple and intuitive in angular-material and you can read all about it here.

Later on, we use layout-gt-sm="row" which overrides column setting on screens greater than 960px wide.

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
<div layout="column" layout-padding>
  <party-add ng-show="partiesList.isLoggedIn"></party-add>
  <div ng-hide="partiesList.isLoggedIn">
    <i>Log in to create a party!</i>
  </div>
 
  <div flex>
    <h2 class="md-display-1">List of the parties:</h2>
  </div>
 
  <div flex layout="row" layout-padding>
    <md-input-container>
      <label>Search</label>
      <input ng-model="partiesList.searchText">
    </md-input-container>
 
    <parties-sort on-change="partiesList.sortChanged(sort)" property="name" order="1"></parties-sort>
  </div>
 
  <div layout="column" layout-gt-sm="row">
    <div flex="50">
      <md-card dir-paginate="party in partiesList.parties | itemsPerPage: partiesList.perPage" total-items="partiesList.partiesCount">
        <md-card-title>
          <md-card-title-text>
            <span class="md-headline" ui-sref="partyDetails({ partyId: party._id })">
              {{party.name}}
              <party-remove party="party"></party-remove>
            </span>
            <span class="md-subhead">{{party.description}}</span>
          </md-card-title-text>
        </md-card-title>
        <md-card-content>
          <party-rsvps-list rsvps="party.rsvps"></party-rsvps-list>
 
          <party-unanswered party="party" ng-if="!party.public"></party-unanswered>
          <div ng-if="party.public">
            Everyone is invited
          </div>
 
          <party-creator party="party"></party-creator>
        </md-card-content>
        <md-card-actions>
          <party-rsvp party="party" ng-show="partiesList.isLoggedIn"></party-rsvp>
          <div ng-hide="partiesList.isLoggedIn">
            <i>Sign in to RSVP for this party.</i>
          </div>
        </md-card-actions>
      </md-card>
      <dir-pagination-controls on-page-change="partiesList.pageChanged(newPageNumber)"></dir-pagination-controls>
    </div>
    <div flex="50">
      <parties-map parties="partiesList.parties"></parties-map>
    </div>
  </div>
18.10 Leave only one definition imports/ui/components/partiesList/partiesList.less
1
2
3
4
5
parties-list {
  [ui-sref] {
    cursor: pointer;
  }
}

PariesMap

Remove the heading:

1
2
3
<div class="angular-google-map-container">
  <ui-gmap-google-map center="partiesMap.map.center" zoom="partiesMap.map.zoom">
    <ui-gmap-markers models="partiesMap.parties" coords="'location'" fit="true" idkey="'_id'" doRebuildAll="true"></ui-gmap-markers>
1
2
3
4
5
6
parties-map {
  display: block;
  padding: 10px;
 
  .angular-google-map-container {
    width: 100%;
3
4
5
6
7
    cursor: pointer;
  }
}
 
@import "../partiesMap/partiesMap.less";

PartiesSort

Let's use md-input-container combined with md-select:

1
2
3
4
5
6
7
8
9
10
<md-input-container>
  <md-select ng-model="partiesSort.order" ng-change="partiesSort.changed()">
    <md-option value="1">
      Ascending
    </md-option>
    <md-option value="-1">
      Descending
    </md-option>
  </md-select>
</md-input-container>

PartyAdd

We won't be using partyAdd.less any longer, so it can be removed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div layout="column" layout-padding>
  <md-input-container>
    <label>
      Party Name:
    </label>
    <input type="text" ng-model="partyAdd.party.name"/>
  </md-input-container>
  <md-input-container>
    <label>
      Description:
    </label>
    <input type="text" ng-model="partyAdd.party.description"/>
  </md-input-container>
  <div flex>
    <md-checkbox ng-model="partyAdd.party.public" aria-label="Public Party?">
      Public Party?
    </md-checkbox>
  </div>
  <div flex>
    <md-button ng-click="partyAdd.submit()" class="md-raised">Add Party!</md-button>
  </div>
</div>

PartyCreator

We can now remove partyCreator.less since we no longer need it.

1
2
3
4
5
<p>
  <small>
    Posted by {{ partyCreator.creator | displayNameFilter }}
  </small>
</p>

PartyRemove

Let's use clear icon from content set:

1
<md-icon md-svg-icon="content:ic_clear_24px" ng-click="partyRemove.remove()"></md-icon>

Move component to the right:

1
2
3
party-remove {
  float: right;
}
5
6
7
8
}
 
@import "../partiesMap/partiesMap.less";
@import "../partyRemove/partyRemove.less";

PartyRsvp

Since we using only the angular-material directives you can remove .less file of PartyRsvp component:

1
2
3
4
5
<div layout="row" layout-align="end center">
  <md-button ng-click="partyRsvp.yes()" ng-class="{'md-primary' : partyRsvp.isYes()}">I'm going!</md-button>
  <md-button ng-click="partyRsvp.maybe()" ng-class="{'md-primary' : partyRsvp.isMaybe()}">Maybe</md-button>
  <md-button ng-click="partyRsvp.no()" ng-class="{'md-primary' : partyRsvp.isNo()}">No</md-button>
</div>

PartyUninvited

Thanks to the angular-material we no longer need partyUninvited.less:

1
2
3
4
5
6
7
8
9
10
11
12
<h4 class="md-headline">
  Users to invite:
</h4>
 
<md-list>
  <md-list-item ng-repeat="user in partyUninvited.users | uninvitedFilter:partyUninvited.party" ng-click="partyUninvited.invite(user)">
    <p>{{ user | displayNameFilter }}</p>
  </md-list-item>
  <md-list-item ng-if="(partyUninvited.users | uninvitedFilter:partyUninvited.party).length <= 0">
    Everyone are already invited.
  </md-list-item>
</md-list>

PartyRsvpsList

Let's import styles in PartiesList:

18.28 Add PartyRsvpsList less to PartiesList imports/ui/components/partiesList/partiesList.less
6
7
8
9
 
@import "../partiesMap/partiesMap.less";
@import "../partyRemove/partyRemove.less";
@import "../partyRsvpsList/partyRsvpsList.less";

PartyMap

Add a little padding from each side:

18.29 Update styles of PartyMap imports/ui/components/partyMap/partyMap.less
1
2
3
4
5
6
party-map {
  display: block;
  padding: 10px;
 
  .angular-google-map-container {
    width: 100%;

PartyDetails

As you can see, we used md-input-container in similar way as we did with PartyAdd:

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
<div layout="column" layout-padding>
  <div layout="column" layout-gt-sm="row"  layout-padding>
    <form flex="50" layout="column">
      <md-input-container>
        <label>Party name</label>
        <input ng-model="partyDetails.party.name" ng-disabled="!partyDetails.isOwner">
      </md-input-container>
 
      <md-input-container>
        <label>Description</label>
        <input ng-model="partyDetails.party.description" ng-disabled="!partyDetails.isOwner">
      </md-input-container>
 
      <div>
        <md-checkbox ng-model="partyDetails.party.public" ng-disabled="!partyDetails.isOwner" aria-label="Public Party?">
          Public Party?
        </md-checkbox>
      </div>
 
      <div>
        <md-button ng-click="partyDetails.save()" class="md-primary md-raised">Save</md-button>
      </div>
    </form>
    <party-map flex="50" location="partyDetails.party.location"></party-map>
  </div>
 
  <party-uninvited flex party="partyDetails.party" ng-show="partyDetails.canInvite()"></party-uninvited>
</div>

Make partyDetails.less to look like this:

18.31 Leave only PartyMap inside PartyDetails less imports/ui/components/partyDetails/partyDetails.less
1
@import "../partyMap/partyMap.less";

Modal window for PartyAdd

It would be great to move PartyAdd outside PartiesList. It would be even greater to make modal window for it.

We need to create some sort of modal window trigger:

18.32 Create view for PartyAddButton imports/ui/components/partyAddButton/partyAddButton.html
1
2
3
<md-button class="md-fab" aria-label="Add new party" ng-click="partyAddButton.open($event)">
  <md-icon md-svg-icon="content:ic_add_24px"></md-icon>
</md-button>
18.33 Create PartyAddButton component imports/ui/components/partyAddButton/partyAddButton.js
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
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import buttonTemplate from './partyAddButton.html';
import modalTemplate from './partyAddModal.html';
import { name as PartyAdd } from '../partyAdd/partyAdd';
 
class PartyAddButton {
  constructor($mdDialog, $mdMedia) {
    'ngInject';
 
    this.$mdDialog = $mdDialog;
    this.$mdMedia = $mdMedia
  }
 
  open(event) {
    this.$mdDialog.show({
      controller($mdDialog) {
        'ngInject';
 
        this.close = () => {
          $mdDialog.hide();
        }
      },
      controllerAs: 'partyAddModal',
      template: modalTemplate,
      targetEvent: event,
      parent: angular.element(document.body),
      clickOutsideToClose: true,
      fullscreen: this.$mdMedia('sm') || this.$mdMedia('xs')
    });
  }
}
 
const name = 'partyAddButton';
 
// create a module
export default angular.module(name, [
  angularMeteor,
  PartyAdd
]).component(name, {
  template: buttonTemplate,
  controllerAs: name,
  controller: PartyAddButton
});

What we did there?

  • We used $mdDialog to open a new dialog window.
  • We used $mdMedia to check if window has to be opened in fullscreen mode (on small screens it look much better).
  • We defined a view partyAddModal.html which we will create in the next step.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<md-dialog aria-label="New party" ng-cloak>
  <md-toolbar>
    <div class="md-toolbar-tools">
      <h2>New party</h2>
      <span flex></span>
      <md-button ng-click="partyAddModal.close()">
        Close
      </md-button>
    </div>
  </md-toolbar>
  <md-dialog-content>
    <div class="md-dialog-content">
      <party-add done="partyAddModal.close()"></party-add>
    </div>
  </md-dialog-content>
</md-dialog>
1
2
3
4
5
party-add-button {
  position: fixed;
  right: 15px;
  bottom: 15px;
}
As you can see we used done directive on PartyAdd component and for now it does nothing. It would be an expression binding which invokes after a new party has been added.
18.36 Implement `done` expression to PartyAdd imports/ui/components/partyAdd/partyAdd.js
14
15
16
17
18
19
20
21
22
23
24
 
34
35
36
37
38
39
40
41
42
  submit() {
    this.party.owner = Meteor.userId();
    Parties.insert(this.party);
 
    if(this.done) {
      this.done();
    }
 
    this.reset();
  }
 
...some lines skipped...
  angularMeteor
]).component(name, {
  template,
  bindings: {
    done: '&?'
  },
  controllerAs: name,
  controller: PartyAdd
});

Great! Our new component is now working and cooperating with PartyAdd component. Let's implement it into PartiesList:

18.37 Add PartyAddButton to PartiesList imports/ui/components/partiesList/partiesList.js
9
10
11
12
13
14
15
 
77
78
79
80
81
82
83
import { Parties } from '../../../api/parties';
import { name as PartiesSort } from '../partiesSort/partiesSort';
import { name as PartiesMap } from '../partiesMap/partiesMap';
import { name as PartyAddButton } from '../partyAddButton/partyAddButton';
import { name as PartyRemove } from '../partyRemove/partyRemove';
import { name as PartyCreator } from '../partyCreator/partyCreator';
import { name as PartyRsvp } from '../partyRsvp/partyRsvp';
...some lines skipped...
  utilsPagination,
  PartiesSort,
  PartiesMap,
  PartyAddButton,
  PartyRemove,
  PartyCreator,
  PartyRsvp,
1
2
3
4
5
<div layout="column" layout-padding>
  <party-add-button ng-show="partiesList.isLoggedIn"></party-add-button>
 
  <div flex>
    <h2 class="md-display-1">List of the parties:</h2>
6
7
8
9
10
 
@import "../partiesMap/partiesMap.less";
@import "../partyRemove/partyRemove.less";
@import "../partyAddButton/partyAddButton.less";
@import "../partyRsvpsList/partyRsvpsList.less";

Now try to click on a button displayed in the right bottom corner of the screen. You should see opened modal window with PartyAdd component!

Custom Authentication Components

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

First, let's create Auth component:

18.40 Create view for Auth imports/ui/components/auth/auth.html
1
2
3
4
5
6
<div layout="row">
  <md-button flex ui-sref="login" ng-hide="auth.isLoggedIn">Login</md-button>
  <md-button flex ui-sref="register" ng-hide="auth.isLoggedIn">Sign up</md-button>
  <md-button flex ng-click="auth.logout()" ng-show="auth.isLoggedIn">Logout</md-button>
  <div ng-show="auth.isLoggedIn">{{ auth.currentUser | displayNameFilter }}</div>
</div>
18.41 Create Auth component imports/ui/components/auth/auth.js
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 angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
 
import template from './auth.html';
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
 
const name = 'auth';
 
class Auth {
  constructor($scope, $reactive) {
    'ngInject';
 
    $reactive(this).attach($scope);
 
    this.helpers({
      isLoggedIn() {
        return !!Meteor.userId();
      },
      currentUser() {
        return Meteor.user();
      }
    });
  }
 
  logout() {
    Accounts.logout();
  }
}
 
// create a module
export default angular.module(name, [
  angularMeteor,
  DisplayNameFilter
]).component(name, {
  template,
  controllerAs: name,
  controller: Auth
});

As you can see, we're going to create components for few new states.

First state is a page with Login component.

In this component we use Meteor's accounts, and use the Accounts API to login our user with email and password.

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
72
<md-content layout="row" layout-align="center start" layout-fill layout-margin>
 
  <md-whiteframe layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
 
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1">
          Sign in
        </h3>
      </div>
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">
          Use existing account</p>
      </div>
      <div layout="row" layout-fill layout-margin layout-padding layout-wrap>
        <md-button class="md-raised">
          <i class="icon ion-social-google" style="color: #DC4A38; font-size: 24px;"></i>
          <span>
            Google</span>
        </md-button>
        <md-button class="md-raised">
          <i class="icon ion-social-facebook" style="color: #3F62B4; font-size: 24px;"></i>
          <span>Facebook
          </span>
        </md-button>
        <md-button class="md-raised">
          <i class="icon ion-social-twitter" style="color: #27AAE2; font-size: 24px;"></i>
          <span>Twitter
          </span>
        </md-button>
      </div>
      <md-divider class="inset"></md-divider>
 
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">
          Sign in with your email</p>
      </div>
 
      <form name="loginForm" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container>
          <label>
            Email
          </label>
          <input type="text" ng-model="login.credentials.email" aria-label="email" required/>
        </md-input-container>
        <md-input-container>
          <label>
            Password
          </label>
          <input type="password" ng-model="login.credentials.password" aria-label="password" required/>
        </md-input-container>
        <div layout="row" layout-align="space-between center">
          <a class="md-button" href="/password">Forgot password?</a>
          <md-button class="md-raised md-primary" ng-click="login.login()" aria-label="login" ng-disabled="login.loginForm.$invalid()">Sign In
          </md-button>
        </div>
      </form>
      <md-toolbar ng-show="login.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ login.error }}</p>
      </md-toolbar>
      <md-divider></md-divider>
      <div layout="row" layout-align="center">
        <a class="md-button" href="/register">Need an account?</a>
      </div>
 
    </div>
 
  </md-whiteframe>
</md-content>
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
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import { Meteor } from 'meteor/meteor';
 
import template from './login.html';
 
import { name as Register } from '../register/register';
 
class Login {
  constructor($scope, $reactive, $state) {
    'ngInject';
 
    this.$state = $state;
 
    $reactive(this).attach($scope);
 
    this.credentials = {
      email: '',
      password: ''
    };
 
    this.error = '';
  }
 
  login() {
    Meteor.loginWithPassword(this.credentials.email, this.credentials.password,
      this.$bindToContext((err) => {
        if (err) {
          this.error = err;
        } else {
          this.$state.go('parties');
        }
      })
    );
  }
}
 
const name = 'login';
 
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter
])
  .component(name, {
    template,
    controllerAs: name,
    controller: Login
  })
  .config(config);
 
function config($stateProvider) {
  'ngInject';
 
  $stateProvider.state('login', {
    url: '/login',
    template: '<login></login>'
  });
}
18.44 Add Login to Auth imports/ui/components/auth/auth.js
6
7
8
9
10
11
12
 
34
35
36
37
38
39
40
41
 
import template from './auth.html';
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
import { name as Login } from '../login/login';
 
const name = 'auth';
 
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  DisplayNameFilter,
  Login
]).component(name, {
  template,
  controllerAs: name,

In Register component we use Meteor's accounts, and use the Accounts API to add a new user.

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
72
<md-content layout="row" layout-align="center start" layout-fill layout-margin>
  <md-whiteframe layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
 
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1">
          Register a new account</h3>
      </div>
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
 
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">Use your email?</p>
      </div>
 
      <form name="registerForm" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container >
          <label>
            Email
          </label>
          <input type="text" ng-model="register.credentials.email" placeholder="email" aria-label="email" required/>
        </md-input-container>
        <md-input-container >
          <label>
            Password
          </label>
          <input type="password" ng-model="register.credentials.password" placeholder="password" aria-label="password" required/>
        </md-input-container>
        <div layout="row" layout-align="end center">
          <md-button class="md-raised md-primary" ng-click="register.register()" aria-label="login" ng-disabled="register.registerForm.$invalid()">Register</md-button>
        </div>
      </form>
 
      <md-divider class="inset"></md-divider>
 
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">
          Want to use an existing account?
        </p>
      </div>
 
      <div layout="row" layout-fill layout-margin layout-padding layout-wrap>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_google_24px" style="color: #DC4A38;"></md-icon>
          <span>
            Google</span>
        </md-button>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_facebook_24px" style="color: #3F62B4;"></md-icon>
          <span>Facebook
          </span>
        </md-button>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_twitter_24px" style="color: #27AAE2;"></md-icon>
          <span>Twitter
          </span>
        </md-button>
      </div>
      <md-toolbar ng-show="register.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ register.error }}</p>
      </md-toolbar>
 
      <md-divider></md-divider>
      <div layout="row" layout-align="center">
        <a class="md-button" href="/login">Already a user?</a>
      </div>
 
    </div>
 
  </md-whiteframe>
</md-content>
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
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import { Accounts } from 'meteor/accounts-base';
 
import template from './register.html';
 
class Register {
  constructor($scope, $reactive, $state) {
    'ngInject';
 
    this.$state = $state;
 
    $reactive(this).attach($scope);
 
    this.credentials = {
      email: '',
      password: ''
    };
 
    this.error = '';
  }
 
  register() {
    Accounts.createUser(this.credentials,
      this.$bindToContext((err) => {
        if (err) {
          this.error = err;
        } else {
          this.$state.go('parties');
        }
      })
    );
  }
}
 
const name = 'register';
 
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter
])
  .component(name, {
    template,
    controllerAs: name,
    controller: Register
  })
  .config(config);
 
function config($stateProvider) {
  'ngInject';
  $stateProvider.state('register', {
    url: '/register',
    template: '<register></register>'
  });
}
18.47 Add Register to Auth imports/ui/components/auth/auth.js
7
8
9
10
11
12
13
 
36
37
38
39
40
41
42
43
import template from './auth.html';
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
import { name as Login } from '../login/login';
import { name as Register } from '../register/register';
 
const name = 'auth';
 
...some lines skipped...
export default angular.module(name, [
  angularMeteor,
  DisplayNameFilter,
  Login,
  Register
]).component(name, {
  template,
  controllerAs: name,

We also have "Recover" button in the login page, so let's create a component that handles that, and call it Password:

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
<md-content layout="row" layout-align="center start" layout-fill layout-margin>
  <md-whiteframe layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
 
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1"> Reset Password</h3>
      </div>
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
 
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">Enter your email so we can send you a reset link</p>
      </div>
 
      <form name="resetForm" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container>
          <label> Email </label>
          <input type="text" ng-model="password.credentials.email" placeholder="email" aria-label="email" required/>
        </md-input-container>
        <div layout="row" layout-align="end center">
          <md-button class="md-raised md-primary" ng-click="password.reset()" aria-label="reset"
                     ng-disabled="password.resetForm.$invalid()">Send Email
          </md-button>
        </div>
      </form>
 
      <md-toolbar ng-show="password.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ password.error }}</p>
      </md-toolbar>
 
      <md-divider></md-divider>
      <div layout="row" layout-align="center">
        <a class="md-button" href="/login">Sign in</a>
      </div>
 
    </div>
 
  </md-whiteframe>
</md-content>
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
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import { Accounts } from 'meteor/accounts-base';
 
import template from './password.html';
 
class Register {
  constructor($scope, $reactive, $state) {
    'ngInject';
 
    this.$state = $state;
 
    $reactive(this).attach($scope);
 
    this.credentials = {
      email: ''
    };
 
    this.error = '';
  }
 
  reset() {
    Accounts.forgotPassword(this.credentials, this.$bindToContext((err) => {
      if (err) {
        this.error = err;
      } else {
        this.$state.go('parties');
      }
    }));
  }
}
 
const name = 'password';
 
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter
])
  .component(name, {
    template,
    controllerAs: name,
    controller: Register
  })
  .config(config);
 
function config($stateProvider) {
  'ngInject';
 
  $stateProvider.state('password', {
    url: '/password',
    template: '<password></password>'
  });
}
18.50 Add Password to Auth imports/ui/components/auth/auth.js
8
9
10
11
12
13
14
 
38
39
40
41
42
43
44
45
import { name as DisplayNameFilter } from '../../filters/displayNameFilter';
import { name as Login } from '../login/login';
import { name as Register } from '../register/register';
import { name as Password } from '../password/password';
 
const name = 'auth';
 
...some lines skipped...
  angularMeteor,
  DisplayNameFilter,
  Login,
  Register,
  Password
]).component(name, {
  template,
  controllerAs: name,

Since every component is ready, we can now implement Auth into Socially:

18.51 Add Auth to Socially imports/ui/components/socially/socially.js
7
8
9
10
11
12
13
 
21
22
23
24
25
26
27
import { name as PartiesList } from '../partiesList/partiesList';
import { name as PartyDetails } from '../partyDetails/partyDetails';
import { name as Navigation } from '../navigation/navigation';
import { name as Auth } from '../auth/auth';
 
class Socially {}
 
...some lines skipped...
  PartiesList,
  PartyDetails,
  Navigation,
  Auth,
  'accounts.ui'
]).component(name, {
  template,
5
6
7
8
9
10
11
        Socially
      </span>
    </h2>
    <span flex></span>
    <auth></auth>
  </div>
</md-toolbar>

Inside the md-toolbar you see we used

<span flex></span>

element which is actually a separator blank element which is used to fill all the available blank space between the first and third element in the toolbar.

We can now remove navigation.less, which we don't need any longer:

18.54 Remove import statement imports/ui/components/socially/socially.less
2
3
4
5
6
  display: block;
}
 
@import "../partiesList/partiesList.less";
@import "../partyDetails/partyDetails.less";

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

Summary

In this chapter we two main things:

  1. How to work with angular-material-design in our angular-meteor app
  2. How to create custom Angular 1 forms for our application's auth

I hope one of you will create an accounts-ui package based on that code and will save us all tons of code!