AngularJS Material Design Tabs, Forms & Toasts

If you’d like to create the same app that I’m using in the video, here is the list of prior videos in this series:

The series so far:

  1. Setup MEAN.js 0.4.0
  2. Add a MongoDB to your app
  3. Add Angular-Material to your app
  4. Create the AngularJS Material Starter App
  5. AngularJS Material Design Toolbar Examples

We’ll pick this video up from the end of the AngularJS Material Design Toolbar Tips and Tricks tutorial examples. In that post we looked at creating different types of toolbars (amongst other things) using AngularJS Material Design.

It looked something like this:

Tabs-1
(accent has been changed to yellow here)

The AngularJS Material Design team have mentioned that Tabs have been heavily re-factored in the upcoming version. So we’re going to test it out, and have a play with a few other Material Design directives.

The code in this tutorial is based on 0.8.x and will be updated when 0.9 is released.

Here’s a short overview of both the video and this post:

When we do this It’ll look like this
When we select a Tab, the content area should change, using Angular ui-router Tabs-3-Home
When a page is refreshed, the relevant Tab for the view should be highlighted Tabs-4-Contact
In Tab 2, we’ll add a Material Design form Tabs-10-Contact-md-button
Lastly, we’ll display a Toast when the new form is submitted Tabs-11-Contact-md-toast

Let’s change the main content area when a Tab is selected

To get the Tabs to actually ‘work’, you could put the contents of the tab within the tab tags, like this:

<md-tabs>
      <md-tab label="one">
        <md-content>
          <p>This is the First Tab</p>
        </md-content>
      </md-tab>
      <md-tab label="two">
        <md-content>
          <p>This is the Second Tab</p>
        </md-content>
      </md-tab>
</md-tabs>

Look familiar? It’s an example lifted and simplified from the AngularJS Material Design site. It’s a good example, but it doesn’t help when your content for the tabs are in separate template files (as they really should be!).

There are other ways to use the AngularJS Material Design Tabs.

Instead of putting the contents within the md-tabs, we’ll use Angular ui-router to show a different template (html view), when a Tab is selected.

Why?

Because, the tabs are sitting in the header.client.view, but my content isn’t.

Here is the header.client.view:

The hamburger menu is hidden when the screen size is greater-than-small (small being mobile device size).

Tabs-2-heading


<div data-ng-controller="HeaderController" flex>
    <md-toolbar layout="row" layout-align="center end"
                class="md-tall"
                data-ng-controller="HomeController as ul">
        <div flex flex-sm="100" flex-gt-sm="95"
             flex-gt-md="80" layout="column">
            <span layout="row">
                <md-button class="menu" hide-gt-sm 
                           ng-click="ul.toggleList()" 
                           aria-label="Show User List">
                    <md-icon md-svg-icon="menu" ></md-icon>
                </md-button>
                <h2> The Heading </h2>
            </span>
            <span flex></span>
            <md-tabs md-stretch-tabs="always" 
                     class="md-primary md-hue-2">
                <md-tab>
                    <md-tab-label> Tab 1</md-tab-label>
                </md-tab>
                <md-tab>
                    <md-tab-label> Tab 2</md-tab-label>
                </md-tab>
            </md-tabs>
        </div>
    </md-toolbar>
</div>

The First Tab

When the first Tab (Tab 1) is selected, I want the existing home page with the list of avatars to display.

Here is the home.client.view:

The sidebar is displayed when the screen size is greater-than-small (small being mobile device size).

Tabs-3-Home

<div flex layout="row" data-ng-controller="HomeController as ul">

    <!-- Container #3 -->
    <md-sidenav md-is-locked-open="$mdMedia('gt-sm')" 
                md-component-id="left" class="md-whiteframe-z2">
        <md-list>
            <md-list-item ng-repeat="it in ul.users">
                <md-button ng-click="ul.selectUser(it)" 
                           ng-class="{'selected' : it === ul.selected }">
                    <md-icon md-svg-icon="{{it.avatar}}" 
                             class="avatar"></md-icon>
                    {{it.name}}
                </md-button>
            </md-list-item>
        </md-list>
    </md-sidenav>

    <!-- Container #4 -->
    <md-content flex id="content">
        <!-- User details sample -->
        <md-icon md-svg-icon="{{ul.selected.avatar}}" 
                 class="avatar"></md-icon>
        <h2>{{ul.selected.name}}</h2>
        <p>{{ul.selected.content}}</p>

        <md-button class="share" md-no-ink ng-click="ul.share($event)" 
                   aria-label="Share">
            <md-icon md-svg-icon="share" ></md-icon>
        </md-button>
    </md-content>

</div>

You can find the rest of the code, such as the controllers and the avatars here: AngularJS Material Starter App

The Second Tab

When the second Tab (Tab 2) is selected, I want a new page to display. Let’s call this the contact form.

Tabs-4-Contact

Create a new view that will be ‘used’ by Tab 2

Here is the contact-form.client.view:

<div flex layout="row">
     <h1>The super duper awesome Contact Form will go here!</h1>
</div>

So, if you’re still with me, we have the following views:

  • A Header, which contains UI portion of two Tabs (Tab 1 and Tab 2)
  • A home page, which contains a list of avatars – this will display when Tab 1 is selected.
  • A new view, which contains a Heading – this will display when Tab 2 is selected.

Using UI Router

Now that we’ve got a new view, lets add it to our AngularJS routes so that we can use it by referring to its state. State is a UI router concept – you can read about State here.

Add a new Angular Route for the new view (this goes in core.client.routes.js)

		// Home state routing
		$stateProvider.
			state('home', {
				url: '/',
				templateUrl: 'modules/core/views/home.client.view.html'
			}).
			state('contact', {
				url: '/contact',
				templateUrl: 'modules/core/views/contact-form.client.view.html'
			});

Tell your Tabs about your Routes

Now that we have our UI-router Routes, we can hook them up with our Tabs.

We’re using a ui-router directive here:
data-ui-sref This refers to the ‘state’ of the route that we want to use when the tab is selected.

Our current options are ‘home’ and ‘contact’.

Seeing the code might help:

            <md-tabs md-stretch-tabs="always" class="md-primary md-hue-2">
                <md-tab data-ui-sref="home">
                    <md-tab-label> Tab 1 </md-tab-label>
                </md-tab>
                <md-tab data-ui-sref="contact">
                    <md-tab-label> Tab 2 </md-tab-label>
                </md-tab>
            </md-tabs>

Note if you’re using rawGit: There is currently a known issue for ui-router and tabs in AngularJS Material Design 0.9 – Issue 2344

Okay. Now test it out.

Click Tab 1. Tab 2. Tab 2. Tab 1. Tab 2.

Go on. I’ll wait.

Umm… when I select Tab 2 and refresh the page, Tab 1 is highlighted

Yeah. That’s a shame.

Luckily we have a handy little directive that will clear that right up!

md-active This is a handy little material design directive that will show the Tab as ‘Active’ (i.e. selected), when it is set to True.

We’ll set md-active to true and make the Tab active based on the Ui-router state. Specifically $state.is which you can read about here.

Another look at that code:

            <md-tabs md-stretch-tabs="always"
                     class="md-primary md-hue-2">
                <md-tab data-ui-sref="home"
                        md-active="$state.is('home')">
                    <md-tab-label> Tab 1</md-tab-label>
                </md-tab>
                <md-tab data-ui-sref="contact"
                        md-active="$state.is('contact')">
                    <md-tab-label> Tab 2</md-tab-label>
                </md-tab>
            </md-tabs>

Now when you refresh the page, Angular Ui-Router will set the State, and based on a matching State, the active Tab will display with an accent highlight at the bottom of the Tab.

If you hit an error when you’re using $state on the view, just make sure you have included it in your controller (see the example of the header controller here)

Next, we’re going to turn our boring Tab 2 into a simple contact form. And when we Submit the form, we’ll display a little toast.

Create a Contact Form

Bring back the contact-form.client.view.

Now, we’re going to jazz it up with some material design directives.

We’ll center it. Put it in a whiteframe. And make it all pretty.

Center the content

Center the content by using a Row layout, with layout-align="center center". Rows help to set the size of a flex box horizontally.

Tabs-5-Contact-center

<div flex layout="row" layout-align="center center">
     <h1>The super duper awesome Contact Form will go here!</h1>
</div>

Add a basic form

If you’ve used AngularJS before, you’ll be familar with ng-submit and ng-model directives. Nothing crazy going on here:

Tabs-6-Contact-form

<div flex layout="row" layout-align="center center">
    <form name="contactForm" data-ng-submit="sendMail()">
         Name: <input type="text" data-ng-model="contactName">
         Email: <input type="email" data-ng-model="contactEmail">
         Message: <textarea ng-model="contactMsg" columns="1" required></textarea>
         <button type="submit">Send</button>
    </form>
</div>

Let’s Jazz it up with Material Design directives!

Add a responsive whiteframe

This will ‘frame’ our form, and display at different sizes based on the size of the device on which it is being viewed:

Tabs-7-Contact-md-whiteframe

<div flex layout="row" layout-align="center center">
    <div flex-sm="100" flex-gt-sm="90" flex-gt-md="70" flex-gt-lg="50" class="md-whiteframe-z2">
    <form name="contactForm" data-ng-submit="sendMail()">
          Name: <input type="text" data-ng-model="contactName">
          Email: <input type="email" data-ng-model="contactEmail">
          Message: <textarea ng-model="contactMsg" columns="1" required></textarea>
          <button type="submit">Send</button>
    </form>
    </div>
</div>

Add a content area for our form

This will give our form some nice padding around it to help it sit pretty.

Tabs-8-Contact-md-content

<div flex layout="row" layout-align="center center">
    <div flex-sm="100" flex-gt-sm="90" flex-gt-md="70" flex-gt-lg="50" class="md-whiteframe-z2">
        <md-content class="md-padding">
            <div flex-sm="100" flex-gt-sm="80" layout-sm="column">
                <form name="contactForm" data-ng-submit="sendMail()">
                    Name: <input type="text" data-ng-model="contactName">
                    Email: <input type="email" data-ng-model="contactEmail">
                    Message: <textarea ng-model="contactMsg" columns="1" required></textarea>
                    <button type="submit">Send</button>
                </form>
            </div>
        </md-content>
    </div>
</div>

A splash of material design

Now, we’ll add in md-input-container, labeldirectives..

Tabs-9-Contact-md-container

<div flex layout="row" layout-align="center center">
    <div flex-sm="100" flex-gt-sm="90" flex-gt-md="70" flex-gt-lg="50" class="md-whiteframe-z2">
        <md-content class="md-padding">
            <div flex-sm="100" flex-gt-sm="80" layout-sm="column">
                <form name="contactForm" data-ng-submit="sendMail()">
                    <md-input-container>
                        <label>Name:</label>
                        <input ng-model="contactName" required>
                    </md-input-container>
                    <md-input-container flex>
                        <label>Email:</label>
                        <input type="email" ng-model="contactEmail" required>
                    </md-input-container>

                    <md-input-container>
                        <label>Message:</label>
                        <textarea ng-model="contactMsg" columns="1" md-maxlength="150" required></textarea>
                    </md-input-container>

                    <button type="submit">Send</button>
                </form>
            </div>
        </md-content>
    </div>
</div>

And a sprinkle of the md-button directive for good luck. Don’t forget to admire your work.

Tabs-10-Contact-md-button

<div flex layout="row" layout-align="center center">
    <div flex-sm="100" flex-gt-sm="90" flex-gt-md="70" flex-gt-lg="50" class="md-whiteframe-z2">
        <md-content class="md-padding">
            <div flex-sm="100" flex-gt-sm="80" layout-sm="column">
                <form name="contactForm" data-ng-submit="sendMail()">
                    <md-input-container>
                        <label>Name:</label>
                        <input ng-model="contactName" required>
                    </md-input-container>
                    <md-input-container flex>
                        <label>Email:</label>
                        <input type="email" ng-model="contactEmail" required>
                    </md-input-container>

                    <md-input-container>
                        <label>Message:</label>
                        <textarea ng-model="contactMsg" columns="1" md-maxlength="150" required></textarea>
                    </md-input-container>

                    <md-button type="submit" class="md-primary"
                               ng-class="{'md-raised md-hue-1': (contactForm.$dirty && contactForm.$valid) }"
                               aria-label="Save Project">Send</md-button>
                </form>
            </div>
        </md-content>
    </div>
</div>

Let’s Toast!

When we click on the ‘Send’ button, let’s display a little toast to the user.

Tabs-11-Contact-md-toast

Add a Controller As reference to the top of the Form

<div data-ng-controller="ContactFormController as cf" flex layout="row" layout-align="center center">
    <div flex-sm="100" flex-gt-sm="90" flex-gt-md="70" flex-gt-lg="50" class="md-whiteframe-z2">
        <md-content class="md-padding">
            <div flex-sm="100" flex-gt-sm="80" layout-sm="column">
                <form name="contactForm" data-ng-submit="cf.sendMail()">
                    <md-input-container>
                        <label>Name:</label>
                        <input ng-model="cf.contactName" required>
                    </md-input-container>
                    <md-input-container flex>
                        <label>Email:</label>
                        <input type="email" ng-model="cf.contactEmail" required>
                    </md-input-container>

                    <md-input-container>
                        <label>Message:</label>
                        <textarea ng-model="cf.contactMsg" columns="1" md-maxlength="150" required></textarea>
                    </md-input-container>

                    <md-button type="submit" class="md-primary"
                               ng-class="{'md-raised md-hue-1': (contactForm.$dirty && contactForm.$valid) }"
                               aria-label="Save Project">Send</md-button>
                </form>
            </div>
        </md-content>
    </div>
</div>

Create a new Controller File

In this file: modules/core/client/controllers/contact-form.client.controller.js:

Add $mdToast and $animate as dependencies to the controller:

'use strict';

angular.module('core').controller('ContactFormController', ['$scope', '$mdToast', '$animate',
        function($scope, $mdToast, $animate) {

Toast!

When the Send button is clicked, we’ll display the toast:

angular.module('core').controller('ContactFormController', ['$scope', '$mdToast', '$animate',
        function($scope, $mdToast, $animate) {

            //3. we decide where the toast will display on the view
            $scope.toastPosition = {
                bottom: false,
                top: true,
                left: false,
                right: true
            };

            //2. the method looks for the position that we want to display the toast
            $scope.getToastPosition = function() {
                return Object.keys($scope.toastPosition)
                    .filter(function(pos) { return $scope.toastPosition[pos]; })
                    .join(' ');
            };

            //1. The send button will call this method
            this.sendMail = function() {
                $mdToast.show(
                    $mdToast.simple()
                        .content('Thanks for your Message ' + this.contactName + ' You Rock!')
                        .position($scope.getToastPosition())
                        .hideDelay(3000)
                );
            };
        }
    ]);

You could just as easily use this to display an error or success response message in a callback.

Woohoo, you just went through a whole bunch of AngularJS and Angular Material Design concepts, and rocked it out!

Until next time! 🙂

8 Comments

Let me know what you think