TL;DR: In this article, you'll develop a simple application in AngularJS 1.5. This application will show you how to use some of the features discussed in Part 1 of this tutorial to build component-based AngularJS applications. If you want to skip the tutorial and dive straight into the code, the repo is publicly available.
Before AngularJS 1.5, developers relied solely on directives to be able to build reusable components in their applications. Currently, AngularJS 1.5+ offers the ability to use the .component()
method to build isolated and reusable components like React
and Angular
applications.
Introducing SpeakerHang
I admire several developers in the community, and one of them is Nadia Odunayo. She's a Ruby developer and regular conference speaker. She built a pretty popular side project called Speakerline. Speakerline is an open source project to help demystify the tech conference CFP process for new speakers. The project is built in Rails.
SpeakerHang, the project we'll build in this tutorial is largely inspired by Speakerline. It's a simpler version. SpeakerHang displays a list of conference speakers, their details and allows you to add a speaker.
Worthy of note here is that there is no database or external REST API. The speakers are added to an array. The crux of this guide is to teach you how to build a component-based AngularJS application easily.
Visualizing SpeakerHang
The best way to build apps is to visualize how the end product will look. Let's think about SpeakerHang in components. Check out the visualization below:
From the diagram above, our visualization produced four components.
- First component for navigation.
- Second component for Add Speaker form.
- Third component for Add Speaker container.
- Fourth component for a list of speakers.
NOTE: You can break up your app into as many components as you want, but be careful not to complicate the process.
Build SpeakerHang
I have a starter pack already configured for building AngularJS 1.5+ apps. The starter pack ships with webpack, Sass compilation, autoreload, ES6 transpiling, component generation, Angularjs UI-Router, and test files generator. It's a forked and enhanced version of NG6-Starter.
Clone the AngularJS starter pack, cd
into the directory and run npm install
to install all the dependencies.
Run gulp serve
to start the app.
Your app should look like the diagram below:
Before proceeding, I'll like you to delete the following:
- The
app/common
directory. - This line in
app/app.js
:import Common from './common/common';
- Remove
Common
from the required dependencies inangular.module
inapp.js
. - The
about
,home
components folder andcomponents.js
file inapp/components
directory. - This line in
app/app.js
:import Components from './components/components';
- Remove
Components
from the required dependencies inangular.module
inapp.js
.
Now, your app should show a blank page. If there are no errors, then you are on track.
Set Up Routes
We'll make use of the efficient UI-Router. Open up app.js
and replace the content with the code below:
import angular from 'angular';
import uiRouter from '@uirouter/angularjs';
import AppComponent from './app.component';
import 'normalize.css';
import 'skeleton.css/skeleton.css';
import './app.scss';
angular.module('app', [
uiRouter
])
.config(($locationProvider, $stateProvider, $urlRouterProvider) => {
"ngInject";
$stateProvider
.state('app', {
url: '/app',
abstract: true,
template: '<app></app>'
})
.state('home', {
url: '/home',
template: '<h4>The Home of Speakers in the Developer Community</h4>'
});
// Default page for the router
$urlRouterProvider.otherwise('/home');
})
.component('app', AppComponent);
In the code above, we defined the home
and app
state. We also configured the default page for the app. The default page that will be rendered when a user visits the app is the /home
route which renders the template.
Note: The template can be a component or simply a string.
Your app should render like this below:
Craft Components - NavBar
It's time to build our components. Earlier, we visualized the SpeakerHang app and came up with four components.
- NavBar component
- Add Speaker container component
- Speaker form component
- Speaker list component
Let's start with the navbar component.
Run the following command in your terminal to generate a component:
gulp component --name navbar
This command will generate a new component, navbar
, inside the components
folder with the following files:
- navar.component.js
- The navbar component itself
- navbar.html
- The navbar template
- navbar.scss
- The style for the navbar template
- navbar.js
- The navbar module that ties everything together
- navbar.controller.js
- The navbar controller that defines the business logic
- navbar.spec.js
- The navbar test file
The first step is to add meat to the navbar template. Replace the content with this:
<div class="navigation">
<ul>
<li><a href="#" class="heading">SpeakerHang</a></li>
<div class="inline align-right">
<li><a ui-sref="speakers">Speakers</a></li>
<li><a ui-sref="addspeaker">Add A Speaker</a></li>
</div>
</ul>
</div>
<hr>
Next, head over to app.js
to import the navbar component.
app/app.js
...
import NavBarComponent from './components/navbar/navbar';
...
angular.module('app', [
uiRouter,
NavBarComponent
])
.config(.....)
.component('app', AppComponent);
We have imported the navbar component and registered it with our app's module. However, we need to take one more step to make sure it reflects on the page.
Call the navbar
component in app/app.html
.
app/app.html
<!-- Place all UI elements intended to be present across all routes in this file -->
<div class="container">
<navbar></navbar>
<div ui-view></div>
</div>
Your app should show the navbar now. Awesome!!!
Speaker List Component
Let's create the speaker list component. Run the component creation command in the terminal like so:
gulp component --name speakerlist
Now, we have the speakerlist
components folder. Take a brief pause here.
The speaker list component should list all the speakers on the platform. The question now is, Where are the speakers?, Where will that data come from?
In a production app, the data will be made available to our app from an API. In the absence of an API, we'll simply make use of an array and use a service.
Let's create the speaker service. Create a new folder, services
, inside the app
directory. And create a speakerservice.js
file inside the services
folder.
Add code to the file like so:
services/speakerservice.js
function SpeakerService() {
"ngInject";
const speakers = [
{
id: 99,
name: "Prosper Otemuyiwa",
favConf: "Ruby Conf 2016",
noOfConf: 13,
favProgrammingQuote: "A C program is like a fast dance on a newly waxed dance floor by people carrying razors."
},
{
id: 100,
name: "Funsho Okubanjo",
favConf: "Laracon 2016",
noOfConf: 7,
favProgrammingQuote: "Don’t worry if it doesn’t work right. If everything did, you’d be out of a job."
},
{
id: 101,
name: "Chilezie Unachukwu",
favConf: "ReactRally 2017",
noOfConf: 19,
favProgrammingQuote: "Good design adds value faster than it adds cost"
},
{
id: 102,
name: "Damilola Adekoya",
favConf: "Codefest Russia 2012",
noOfConf: 25,
favProgrammingQuote: "Talk is cheap. Show me the code."
},
];
return {
// Will retrieve our speakers list for displaying
getAllSpeakers() {
return speakers;
},
// Creating a Speaker entry based on user input.
addASpeaker(speaker) {
const {name, favConf, noOfConf, favProgrammingQuote } = speaker;
const newSpeaker = { name, favConf, noOfConf, favProgrammingQuote };
speakers.push(tempSpeaker);
}
}
}
export default SpeakerService;
In the service above, we have a factory with two functions, getAllSpeakers
responsible for returning a list of speakers and addASpeaker
, responsible for adding a speaker.
Now, let's get back to the speakerlist
component. Open up app/components/speakerlist/speakerlist.component.js
file.
In the speakerlistComponent
object, we have bindings
, template
, controller
, and restrict
.
...
let speakerlistComponent = {
restrict: 'E',
bindings: {},
template,
controller
};
...
restrict
- This means the component should be restricted to an element.
bindings
- This helps specify the binding option whether it's a one-way or two-way data binding. It's represented by a symbol. E.g
<
represents a one-way data binding.
- This helps specify the binding option whether it's a one-way or two-way data binding. It's represented by a symbol. E.g
template
- This is where we specify the view for the component.
controller
- This is where we specify the controller which holds the logic for the component. By default, it ships with
$ctrl
as an alias that can be used in the view to invoke controller methods.
- This is where we specify the controller which holds the logic for the component. By default, it ships with
controllerAs
- This allows developers to specify their alias, e.gvm
as commonly used in a lot of apps.
Update the speakerlistComponent
object to have a controllerAs
key and value like so:
...
let speakerlistComponent = {
restrict: 'E',
bindings: {},
template,
controller,
controllerAs: 'vm'
};
...
Open up app/components/speakerlist/speakerlist.controller.js
and modify it like so:
class SpeakerlistController {
constructor(SpeakerService) {
"ngInject";
// This will keep the service instance across our class
this.SpeakerService = SpeakerService;
this.speakers = [];
}
// This method will be called each time the component will be initialised,
// In our case, it will be called for every page route change.
$onInit() {
this.speakers = this.SpeakerService.getAllSpeakers();
}
}
export default SpeakerlistController;
In the code above, we injected the SpeakerService
into the controller and assigned it to an instance variable.
The $onInit()
lifecycle hook was invoked to initialize the speakers' array with the list of speakers from the speaker service. The $onInit()
hook is good for a controller's initialization code.
Update the speaker view, speakerlist.html
, to have the necessary code to display the list of speakers.
speakerlist.html
<section>
<div class="row">
<div class="row" ng-repeat="speaker in vm.speakers">
<div class="four columns">
<label for="control-label col-sm-2">Speaker Name:</label>
<input class="u-full-width" type="text" placeholder="{{ speaker.name }}" readonly>
</div>
<div class="four columns">
<label for="control-label col-sm-2">No. Of Conferences Attended: </label>
<input class="u-full-width" type="text" placeholder="{{ speaker.noOfConf }}" readonly>
</div>
<div class="four columns">
<label for="favConference">Favorite Conference So Far:</label>
<input class="u-full-width" type="number" placeholder="{{ speaker.favConf }}" readonly>
</div>
<label for="FavoriteProgrammingQuote">Favorite Programming Quote:</label>
<textarea class="u-full-width" placeholder="{{ speaker.favProgrammingQuote }}" readonly></textarea>
<hr/>
</div>
</div>
</section>
Now, we need to import the speaker list component in the app.js
file and define a route for /speakers
.
Open up app/app.js
, import the speakerlist
component and define the /speakers
route.
...
import SpeakerListComponent from './components/speakerlist/speakerlist';
...
angular.module('app', [
uiRouter,
NavBarComponent,
SpeakerListComponent
])
.config(...
...
// Speaker page to contain list of speakers
.state('speakers', {
url: '/speakers',
template: '<speaker-list></speaker-list>'
})
...
)
.component('app', AppComponent);
There is one more step before we can see the list of speakers on the page. Open up app/app.js
and import the speaker service.
...
import SpeakerService from './services/SpeakerService';
...
Now, call the .factory
method on the angular module and reference the speaker service like so:
app/app.js
...
angular.module('app', [
uiRouter,
NavBarComponent,
SpeakerListComponent,
AddSpeakerComponent,
])
.config(...) => {
...
})
.component('app', AppComponent)
.factory('SpeakerService', SpeakerService);
Note: Make sure it is speakerList
, not speakerlist
in .component('speakerList', speakerlistComponent)
section of the app/components/speakerlist/speakerlist.js
file.
Your app should look like this now:
Add Speaker Component
Let's create the add speaker component. Run the component creation command in the terminal like so:
gulp component --name addspeaker
The addspeaker
component is the container for the forthcoming speakerform
component.
Head over to app/app.js
and import the addspeaker
component. We'll also add the route for /add-speaker
.
...
import AddSpeakerComponent from './components/addspeaker/addspeaker';
...
angular.module('app', [
uiRouter,
NavBarComponent,
SpeakerListComponent,
AddSpeakerComponent,
])
.config(($locationProvider, $stateProvider, $urlRouterProvider) => {
...
//Create route for our goat listings creator
.state('addspeaker', {
url: '/add-speaker',
template: '<add-speaker></add-speaker>'
});
// Default page for the router
$urlRouterProvider.otherwise('/home');
})
.component('app', AppComponent)
.factory('SpeakerService', SpeakerService);
Click on the Add A Speaker
link on the navbar. You'll be directed to the /addspeaker
route. Right now, it's empty but everything is working perfectly.
Note: We need to add a speaker form to the template of the addspeaker
component. We'll come back to it once we are done with creating the speaker form component.
Speaker Form Component
Let's create the speaker form component. Run the component creation command in the terminal like so:
gulp component --name speakerform
Open up app/components/speakerform/speakerform.html
and add the form code to it like so:
<form role="form" ng-submit="vm.addSpeaker()">
<div class="row">
<div class="six columns">
<label for="control-label col-sm-2">Speaker Name:</label>
<input class="u-full-width" type="text" placeholder="John Doe" ng-model="vm.speaker.name">
</div>
<div class="six columns">
<label for="control-label col-sm-2">No. Of Conferences Attended:</label>
<input class="u-full-width" type="number" placeholder="25" ng-model="vm.speaker.noOfConf">
</div>
</div>
<label for="favConference">Favorite Conference So Far:</label>
<input class="u-full-width" type="text" placeholder="All Superpower Dev Conf 2050" ng-model="vm.speaker.favConf">
<label for="FavoriteProgrammingQuote">Favorite Programming Quote</label>
<textarea class="u-full-width" placeholder="Organizational Skills Beat Algorithmic Wizadry - James Hague" ng-model="vm.speaker.favProgrammingQuote"></textarea>
<input class="button-primary" type="submit" value="Submit">
</form>
In the code above, we have the addSpeaker()
function that will be invoked when the form is submitted. The data of each input type will be submitted to the speaker
object, made possible via ng-model
.
Let's add the controller code to app/components/speakerform/speakerform.component.js
file.
speakerform.component.js
class SpeakerformController {
constructor($state, SpeakerService) {
"ngInject";
this.$state = $state;
this.SpeakerService = SpeakerService;
this.speaker = {};
}
// will handle the form submission,
// validates the required field and then adds the goat to the service.
// once added, we'll be directed to the next page.
addSpeaker() {
if(!this.speaker.name) return alert('Speaker Name is Required');
if(!this.speaker.noOfConf) return alert('No. Of Conferences Attended is required');
if(!this.speaker.favConf) return alert('Favorite Conference So Far is required');
if(!this.speaker.favProgrammingQuote) return alert('Favorite Programming Quote is required');
this.SpeakerService.addASpeaker(this.speaker);
// reset the form
this.speaker = {};
// go to home page, to see our entry
this.$state.go('speakers');
}
}
export default SpeakerformController;
In the code above, we injected the state
and SpeakerService
into the controller. The addSpeaker()
function collects the values of the form and sends them to the addSpeaker
function.
Quickly add the controllerAs
key with the value of vm
to the speakerform
component.
app/components/speakerform/speakerform.component.js
import template from './speakerform.html';
import controller from './speakerform.controller';
import './speakerform.scss';
let speakerformComponent = {
restrict: 'E',
bindings: {},
template,
controller,
controllerAs: 'vm'
};
export default speakerformComponent;
Now, go ahead and import the speakerform
component in addspeaker.js
like so:
import angular from 'angular';
import addspeakerComponent from './addspeaker.component';
import speakerForm from '../speakerform/speakerform';
let addspeakerModule = angular.module('addspeaker', [
speakerForm
])
.component('addSpeaker', addspeakerComponent)
.name;
export default addspeakerModule;
Note: Make sure it is speakerForm
, not speakerform
in .component('speakerForm', speakerformComponent)
section of the app/components/speakerform/speakerform.js
file.
One more thing, remember the app/components/addspeaker/addspeaker.html
file? It's time to add the <speaker-form>
component to it.
app/components/addspeaker/addspeaker.html
<div>
<speaker-form></speaker-form>
</div>
Try to add a speaker now. The speaker form works and it adds a new speaker to the list of speakers. Yaay!!!
Adding Authentication to Your AngularJS App
Aside: Authenticate an AngularJS App with Auth0
We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with an Angular application using Auth0. You can clone this sample app from the repo on GitHub.
Sign Up for Auth0
You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 application and API so Auth0 can interface with an Angular app and Node API.
Set Up an Auth0 Application
- Go to your Auth0 Dashboard and click the "create a new application" button.
- Name your new app and select "Single Page Web Applications".
- In the Settings for your new Auth0 app, add
http://localhost:3000/callback
to the Allowed Callback URLs. Click the "Save Changes" button. - If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure you set up your own social keys and do not leave social connections set to use Auth0 dev keys.
Note: Under the OAuth tab of Advanced Settings (at the bottom of the Settings section) you should see that the JsonWebToken Signature Algorithm is set to
RS256
. This is the default for new applications. If it is set toHS256
, please change it toRS256
. You can read more about RS256 vs. HS256 JWT signing algorithms here.
Dependencies and Setup
Once you've cloned the project, install the dependencies for both the AngularJS app and the Node server by running the following commands in the root of your project folder:
$ npm install
Start the app via the express server with:
$ npm start
Find the auth0-variables.js.example
file and remove the .example
extension from the filename. Then open the file:
var AUTH0_CLIENT_ID = '{CLIENT_ID}';
var AUTH0_DOMAIN = '{DOMAIN}';
var AUTH0_CALLBACK_URL = 'http://localhost:3000/callback';
Change the AUTH0_DOMAIN
identifier to your Auth0 application domain.
Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.
Change the AUTH0_CLIENT_ID
to your Auth0 information.
let's take a look at how authentication is implemented.
Authentication Service
Authentication logic on the front end is handled with an AuthService
authentication service: src/app/auth/auth.service.js
file.
(function () {
'use strict';
angular.module('app').service('authService', authService);
authService.$inject = ['$state', 'angularAuth0', '$timeout'];
function authService($state, angularAuth0, $timeout) {
function login() {
angularAuth0.authorize();
}
function handleAuthentication() {
angularAuth0.parseHash(function (err, authResult) {
if (authResult && authResult.accessToken && authResult.idToken) {
setSession(authResult);
$state.go('home');
} else if (err) {
$timeout(function () {
$state.go('home');
});
console.log(err);
alert(
'Error: ' + err.error + '. Check the console for further details.',
);
}
});
}
function setSession(authResult) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime(),
);
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
}
function logout() {
// Remove tokens and expiry time from localStorage
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
$state.go('home');
}
function isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
return {
login: login,
handleAuthentication: handleAuthentication,
logout: logout,
isAuthenticated: isAuthenticated,
};
}
})();
The login()
method authorizes the authentication request with Auth0 using your config variables. A login page will be shown to the user and they can then log in.
- handleAuthentication: looks for the result of authentication in the URL hash. Then, the result is processed with the parseHash method from auth0.js
- setSession: sets the user's Access Token and ID Token, and the Access Token's expiry time
- logout: removes the user's tokens and expiry time from browser storage
- isAuthenticated: checks whether the expiry time for the user's Access Token has passed
Note: If it's the user's first visit to our app and our callback is on
localhost
, they'll also be presented with a consent screen where they can grant access to our API. A first party client on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.
Login Control
Provide a component with controls for the user to log in and log out.
app/navbar/navbar.html
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Auth0 - AngularJS</a>
<button class="btn btn-primary btn-margin" ui-sref="home">Home</button>
<button
class="btn btn-primary btn-margin"
ng-if="!vm.auth.isAuthenticated()"
ng-click="vm.auth.login()"
>
Log In
</button>
<button
class="btn btn-primary btn-margin"
ng-if="vm.auth.isAuthenticated()"
ng-click="vm.auth.logout()"
>
Log Out
</button>
</div>
</div>
</nav>
Directive
// app/navbar/navbar.directive.js
(function () {
'use strict';
angular.module('app').directive('navbar', navbar);
function navbar() {
return {
templateUrl: 'app/navbar/navbar.html',
controller: navbarController,
controllerAs: 'vm',
};
}
navbarController.$inject = ['authService'];
function navbarController(authService) {
var vm = this;
vm.auth = authService;
}
})();
Depending on whether the user is authenticated or not, they see the Log Out or Log In button. The ng-click
events on the buttons make calls to the authService
service to let the user log in or out. When the user clicks Log In, they are redirected to the login page.
Callback Component
The callback component is where the app is redirected after authentication. This component simply shows a loading message until the login process is completed. After the session is set up, the users are redirected to the /home
route.
// app/callback/callback.controller.js
(function () {
'use strict';
angular.module('app').controller('CallbackController', callbackController);
function callbackController() {}
})();
app/callback/callback.html
<div class="loading">
<img src="assets/loading.svg" alt="loading" />
</div>
Process Authentication Result
When a user authenticates at the login page, they are redirected to your application. Their URL contains a hash fragment with their authentication information. The handleAuthentication
method in the authService
service processes the hash.
Call the handleAuthentication
method in your app's run block. The method processess the authentication hash while your app loads.
// app/app.run.js
(function () {
'use strict';
angular.module('app').run(run);
run.$inject = ['authService'];
function run(authService) {
// Handle the authentication
// result in the hash
authService.handleAuthentication();
}
})();
More Resources
That's it! We have an authenticated AngularJS application with login, logout, and protected routes. To learn more, check out the following resources:
Conclusion
AngularJS 1.5+ provides a component-based architecture to build apps similar to React and Angular. In this tutorial, you've learned how to build and secure an app using the .component
method and the new APIs included in the framework.
In addition, Auth0 can help secure your AngularJS apps with more than just username-password authentication. It provides features like multifactor auth, anomaly detection, enterprise federation, single sign on (SSO), and more. Sign up today so you can focus on building features unique to your app.
Auth0 provides simple and easy to use interfaces to help administrators manage user identities including password resets, as well as creating, provisioning, blocking and deleting users.