The AngularJS Book!
Step by Logical Step
By Nicholas Johnson
Document Version: 1.0.1
Last Updated: 12/12/2017
Welcome Exercises
The goal of this exercise is just to get some Angular running in a browser.
Getting Angular
We download Angular from angularjs.org Alternately we can use a CDN such as the Google Angular CDN.
Activating the compiler
Angular is driven by the template. This is different from other MVC frameworks where the template is driven by the app.
In Angular we modify our app by modifying our template. The JavaScript we write simply supports this process.
We use the ng-app attribute (directive) to tell Angular to begin compiling our DOM. We can attach this attribute to any DOM node, typically the html or body tags:
<body ng-app>
Hello!
</body>
All the HTML5 within this directive is an Angular template and will be compiled as such.
Exercise 1 - Hello Universe
Let’s start with the simplest thing possible. Hello World.
- Download Angular latest uncompressed from here: https://angularjs.org/
- Concatenate the strings “Hello” and “World” to make a hello world app.
- Create an Angular template which tells you how many seconds there are in a day, a year, a century.
- Find out how many weeks there are in a human lifetime.
Exercise 2 - Visual Studio Setup (if you are using VS)
If you need to develop in Visual Studio, you may have a bit of a culture shock when you start using Angular. We use static HTML templates and compile in the browser using JavaScript. The role of the server is dramatically reduced.
Visual Studio 2015 has excellent support for Angular. MS TypeScript is the language of Angular 2, and VS 2015 has Gulp and Node built right into it.
You may however have to adjust your thinking just a little bit, and you will have rather more hoops to jump through.
Creating the Project
- First create a new project.
- From Templates, create an ASP.Net Web Application.
- Choose Empty to create a completely empty application. We won’t be using any of the features of .Net in our front end application.
Create the HTML file
- Right click your new application, add new item, and create an html file. Call it index.html. This is our template.
- Insert a little bit of text inside it.
- Now right click the file and open in browser. See the text?
- Alt tab back to Visual Studio and make a change to the text.
- Now alt tab back to your web browser. Press refresh. See the change you made?
Getting Angular
We can use NuGet to install Angular.
- Right click the project in the solution explorer and choose manage NuGet packages.
- Choose Angular Core from the list. It will be installed into your Scripts folder. Have a look there now.
Linking Angular
Now we need to link Angular. Because this is the front end we do this with a script tag right in the html.
<script src="/Scripts/angular.js"></script>
Now Attempt the Hello Universe exercise.
Optional Extension - if you are first to finish
You’ve downloaded Angular. Open it in your editor and have a quick browse through the Angular codebase. You’ll find it clean, and well commented and easy to scan.
Binding
Angular features super easy two way binding. Binding a value means setting up listeners so when the value changes, the front end updates to match, and when the front end is changed (say using a form) the saved value is updated.
$scope
Angular uses a special object called $scope
to store data. All bound values are actually attributes of the $scope
object. We will look at $scope
in some detail shortly.
ng-model
We can bind an input to a model using the ng-model directive. This automatically creates watchers (listeners) on the input element. When the input element changes the watchers fire and set an attribute of a special object called $scope
.
<body ng-app>
<input ng-model="test" />
</body>
$scope
is available elsewhere in the view, and also in the controller as we shall very soon see.
Curlies {{}}
We can output any attribute we have in $scope
using the handlebars curly brace syntax. When we do this, Angular will create more watchers in $scope
that recompile the DOM when the ‘test’ attribute changes.
Like so:
<body ng-app>
<input ng-model="test" />
{{test}}
</body>
ng-bind
We can also explicitly bind any element to scope using the ng-bind directive. In most circumstances this is entirely equivalent to the curly brace syntax.
<p ng-bind="test"></p>
Exercise - Evil Siri
You are a crazy scientist building an AI for world domination. For reasons known only to you, you have chosen Angular to help you in this epic task. Your first job is to teach your new creation to greet you by name.
Write an Angular template which contains an “enter your name” form. As you type, it updates the page with an h1 tag, like this:
<h1>Welcome Dave. How may I serve you today?</h1>
Exercise - Handy instant maths calculators
As an evil genius, you need a calculator to help you work how many helidrones you will need to steal the houses of parliament.
Let’s build one now. There are two input fields, each with a number, like this:
<input placeholder="Enter an evil number" /> <input placeholder="And another" />
Use the curly brace syntax or ng-bind to output the sum of the two numbers.
When you fill in the fields, output a little list in HTML that looks something like this:
<ul>
<li>2 + 2 = 4</li>
<li>2 - 2 = 0</li>
<li>2 * 2 = 4</li>
<li>2 / 2 = 1</li>
</ul>
- 3 + 2 = 5
- 3 - 2 = 1
- 3 * 2 = 6
- 3 / 2 = 1.5
When I type in the input fields the list should update in real time.
- You might like to extend this to create a VAT calculator, a currency converter or a unit converter.
Exercise - Time remaining
In the last section we wrote code to let us see the number of seconds in a year.
Add a little input box that lets us type in a your age in years.
Output the number of weeks in your life so far. Have it output the approximate number of weeks remaining. Output a percentage completion.
More on binding
We can bind an attribute of $scope to anything in the DOM, an element, an attribute, a class name, even a portion of an attribute value.
Binding to an attribute
Say we have a value bound to an input element. We could use that value to form part of a style attribute:
<input ng-model="color" />
<p style="color: {{color}}">Hello</p>
Binding to a class
We can use values in scope to construct a class attribute using the ng-class directive. A class attribute can have many values, so we pass an object and all values that evaluate to true are included in the compiled class.
<body ng-class="{red: color=='red', blue: color=='blue' }"></body>
Binding visibility
We can conditionally show and hide elements using ng-show.
<input type="checkbox" ng-model="happyCat" />
<div ng-show="happyCat">Cat is happy</div>
<div ng-show="!happyCat">Cat is grumpy</div>
Exercise - Profile Widget
You are an entrepreneur, creating the next LinkedIn. Users of your awesome service have said that they would like to be able to choose a profile picture just by entering a URL.
We are going to allow people to type in a profile picture url and have that picture appear immediately. Create a field and bind it to a model called something like “avatarUrl”.
Now use the avatarUrl as the src attribute of an img tag, like so:
<img src="" />
Exercise - Profile form
Extend your app so you can type in your name. This will update the the alt text on the image, and an h2 somewhere on the page.
Add in a description textarea. This will update the a paragraph of description text under the image.
If you were to back this with an API, this could be a real site component.
Exercise - Make the form optional
We are going to hide the edit fields when they are not needed so that we either see the edit form or the profile.
Add in a checkbox. Bind it using ng-model. When the checkbox is checked, the edit fields are visible. When is is not checked, the edit fields are hidden. Use ng-show to make this work.
Optional Exercise - Tabs
Use ng-show to create a simple navigation. Your page will contain several sections, each with an ng-show attribute. Choose a horizontal or vertical nav. Use ng-click to set the value of a model and show the sections.
Further reading
- input[checkbox] - https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D
- ngClass - https://docs.angularjs.org/api/ng/directive/ngClass
- Check out the other built in Angular directives here: https://docs.angularjs.org/api/ng/directive
Controllers
AngularJS, as originally designed gave us an MVC pattern. MVC looks like this:
- Model - our data, typically JSON objects.
- View - the template - HTML5.
- Controller - a JavaScript object that mediates between the two.
So far, we have only dealt with the view (the HTML5). In this section we will start to look at controllers, and how we can use them to mediate between our view and our data.
Creating a controller
We initialise a controller by chaining .controller
onto our module definition. The .controller
function receives a string, which is the name of our controller, and a constructor function, which will be used to build the controller.
A controller looks something like this:
angular.module("app", []).controller("DemoController", function ($scope) {
$scope.hello = "World";
});
We hook this into our template something like this:
<div ng-app="app" ng-controller="DemoController">{{hello}}</div>
$scope
Notice that the controller constructor receives a parameter called $scope.
scope. We can also store helper functions here.
The primary purpose of the controller is to initialise $scope
I’ll say again: The primary job of the controller is to initialize $scope. Not to do AJAX (we use services for this). Not to make DOM changes (we use Directives for this). If you find your controller getting large and trying to manage too much, you probably need to reconsider. We’ll look more at this later in the course.
Adding Helper Methods to a Controller
If we store helper methods on our controller, those will be available in the front end:
angular.module('app', [])
.controller("DemoController", function($scope) {
$scope.sayHello = function() {
$scope.greeting = "Hello"!
}
});
We hook this into our template something like this:
<div ng-app="app" ng-controller="DemoController">
<a ng-click="sayHello()">Say Hello</a>
{{greeting}}
</div>
When to create a controller
I generally expect to create one controller per unit of interaction. This means we typically create quite a lot of controllers. For example, you might have a loginFormController, a profileController, a signupController, etc. Many small controllers are better than one massive multi-purpose monolith.
Controller scope
A controller will apply to an html5 element and all of it’s children. We can nest controllers inside each other, in which case the child controller takes precedence if there is a conflict. This is decided using prototypical inheritance. More on this when we get to the section on $scope.
Exercise - Control yourself
We are going to add a profileController to your super advanced profile form from the previous exercise.
- Create a profileController. Use ng-controller to add it to your profile form.
- In your controller, set a default name, description and profile picture URL.
Exercise - Helper function
Extend the calculator exercise. We are going to create a function to allow us to zero all the values.
- Create a function in your controller which zeros number1 and number2. Add it to $scope. It is now available in your front end.
- Add a button to your DOM and use ng-click to call the function.
Remember you never need to refer to scope is always implied.
Further Reading
Read the controller guide here: https://docs.angularjs.org/guide/controller
$scope and the $scope tree
$scope is a shared object which we use to pass data back and forthe between our controllers and our front end. We can make it available in our controller by injecting it, and it is always magically available in our template.
(Note, there is no magic, and we’ll look more at expression compilation in a bit.)
What goes in $scope?
We can store objects such as models, helper functions and even the controller itself as attributes of $scope.
Getting $scope in a controller
We get access to scope, means our controller constructor function receives a variable called $scope:
angular.module("app", []).controller("MyController", function ($scope) {
$scope.hello = "Hey from $scope!";
});
Accessing $scope from the template
We can then show attributes of $scope in the front end, like so:
<p>{{hello}}</p>
Note that we never refer to $scope in the front end. $scope is implicit, because in our template, $scope is all we have. When we access what looks like a variable in our template, we are really accessing an attribute of $scope.
Storing functions in $scope
We can also store functions in $scope. This lets us create callbacks that might be executed when a button is clicked for example, like so:
angular.module("app", []).controller("MyController", function ($scope) {
$scope.handleClick = function () {
$scope.buttonWasClicked = true;
};
});
We might then trigger this helper function when a button is clicked using ng-click like so:
<button ng-click="handleClick()">Click me!</button>
<p>{{buttonWasClicked}}</p>
Notice that we have to call the function in the ng-click expression using braces ().
Scope of $scope
You might be wondering in which parts of our template $scope is available.
Whenever we use the ng-controller directive we get a new scope.
$scope forms a Tree
Each new scope objects that roughly follows the shape of our DOM. At the root of this tree is an object called $rootScope.
If we set a value on scope in our controller, it is available in the current scopes
Exercise - Fix the code
The following pieces of code are suffering from scope issues. Have a look inside and see if you can fix the code for exercise_1 and exercise_2.
Find specific instructions in the readme.txt files.
You can find the exercises in the Github respository.
Exercise - Multiple profile widgets
Extend the profile exercise from the previous section. We should be able to have multiple profile widgets on the same page. Copy and paste a few profile widgets next to each other. Type in one. Notice how they are separate units of interaction, each with its own controller and $scope.
Now copy paste a few profile widgets inside one another, notice how they inherit, typing in the parent
Harder Exercise - Storing data on the controller
This is a harder, more freeform exercise. Feel free to skip this. We will return to writing code like this later in the course.
Because our controller is just an object, we can store your controller itself in $scope, like this:
.controller('myController', function($scope) {
var vm = $scope.vm = this;
vm.profile = {
name: "Dave"
}
})
This is possible because the controller itself is an object, so we can set attributes of it and use it to store data.
You can now access your controller and associated model in your view, like this:
<p>{{vm.profile.name}}</p>
You don’t need to write code like this, but if you do it leads nicely onto components.
Modify the profile widget so that the data is stored on the controller.
Further Reading
Read the scope documentation here: https://docs.angularjs.org/guide/scope
Watching and Applying
We can watch an attribute of $scope using:
$scope.$watch("test", function (newVal, oldVal) {});
Now whenever the value of $scope.test changes the function will be called.
Watchers and the digest cycle
Watchers are added to the $scope.$$watchers
array. This array contains a list of all current watchers, the expression they are evaluating and the last known value of that expression.
Whenever an Angular watcher fires it triggers something called a $scope
digest. This means that the $scope
tree is traversed and all the watch expressions are evaluated. If any come up ‘dirty’ i.e. changed the corresponding functions are executed.
Digest limit
The digest cycle will repeat over and over until the entire $scope
hierarchy is clean or the digest limit is reached. The default digest limit is 10.
Exercise
We are going to hack some quick validation into our profile form. We’ll see the right way to do validation using directives in a bit.
Extend your profile form with a $scope.person.name
field. Let’s make name mandatory. use watch to watch the ‘person.name’ property of scope.
If the value is not blank, set $scope.errors = {}.
Otherwise set $scope.errors = {name: "should not be blank."}
.
Now in your template, use ng-show to show a nice warning message if errors != false.
Downloads
Dependency Injection
We can inject a dependency into a controller by simply receiving it.
angular.module('demoModule', [])
.controller('demoController', function($log) {
$log.log('Hi!');
}
Modules
We can include a module into another module by placing it’s name between the square braces:
angular.module('app', ['demoModule']);
Exercise - NgDialog
We are going to inject the ngDialog service into a controller. This will allow our controller to create popup dialog boxes.
First go here and grab the ng-dialog.js. Link it in the header of your document in the usual way with a script tag.
Now download the CSS files: ng-dialog.css and ng-dialog-theme-plain.css. Link them using link tags.
http://ngmodules.org/modules/ngDialog
Include it in your app.
We need to include ngDialog as a dependency of the app, like this:
angular.module('app', ['ngDialog'])
Inject into your controller.
Create a little controller and use the ng-controller directive to hook it to the DOM.
<div ng-controller="myController">
Inject the ngDialog service into your controller.
.controller('myController', function($scope, ngDialog) {
})
You now have access to ngDialog.open. Call this according to the documentation to create a dialog box when the page loads: https://github.com/likeastore/ngDialog#api
e.g.
ngDialog.open({
template: '<p>my template</p>',
className: 'ngdialog-theme-plain',
plain: true
});
Exercise - Extension
Create a form. Create a method on scope that opens the dialog. Call the method when the button is pressed.
Have a go at the minification safe DI syntax.
Further extension
If you finish first, have a read through the DI documentation here:
Unit Testing with Karma
Karma is a unit testing framework that works especially nicely with Angular. A unit test will test a single tiny piece of our app. We might write a test for a single controller for example. Unit tests generally don’t care about the template, they only test the code. We can write unit tests for template code, but this is generally a little more complex.
In this section we are going to use Karma to unit test a controller.
Installation
First up we need to install Karma. Karma is a NodeJS module, and Node is a JavaScript runtime that runs in your command line. We will need to have Node installed to run Karma. If you don’t have it, you can grab it from here:
test your installation by typing node
at a command line.
NPM
Once you have Node, you will we use NPM (Node Package Manager) to install a few Karma packages, like so:
npm init
npm install karma --save-dev
npm install -g karma-cli
npm install jasmine-core --save-dev
npm install karma-jasmine --save-dev
npm install karma-chrome-launcher --save-dev
Depending on your exact version of Node and Karma, NPM may complain about missing dependencies. If you see any red error messages, go and get those dependencies too.
Once Karma is installed, you should be able to type karma
at a command line and see a helpful message.
Initialise Karma
Karma needs a karma.conf.js file in the root of your project directory to tell it how to work. Use the command line to navigate to the folder in which you want to work. Now type karma init
. You will be taken through a wizard which will create the karma.conf.js file. Keep all the defaults for now.
karma init
We can now start the Karma runner using:
karma start
Specifying files
Karma may be running, but until we give it some files to test, nothing will happen. We need to tell it where to find our JavaScript. Open up the karma.conf.js file, look for the files
attribute, and change it like so:
files: ["lib/*.js", "js/*.js", "specs/angular-mocks.js", "specs/*.js"];
This is a list of file paths to look at in order. Karma will use this to create a complete version of our app.
Writing a test
A Karma test contains one or more describe blocks, which can be nested. These define groups of functionality. Inside these are our tests, which are defined using the it
function. These tests contain numerous expectations.
If a test fails, the strings in the describe and it blocks will be used to create an error message.
describe("Maths in the universe", function () {
it("should be the case that 1 plus 1 is 2", function () {
expect(1 + 1).toEqual(2);
});
});
Exercise - Check that basic maths works
Download the base calculator app from the Github repository. You will notice that the code has been split up into directories, like so:
- lib - Angular itself (also possibly JQuery, D3, etc). Imported first, just like in our app.
- js - Our code. Karma needs to create an instance of our app in order to test it.
- specs - By convention, Karma tests live in the specs folder. All tests in here will be executed.
Let’s play with Jasmine
We are not going to test anything useful here, we are just going to play with Jasmine. Create a file in the spec folder. Call it maths_tests.js.
- Now write a simple test to demonstrate that 1 + 1 == 2.
- Now test that 1 + 1 != 3
You can view the Jasmine documentation here: http://jasmine.github.io/2.1/introduction.html
Testing the actual app.
To test an Angular controller, we have to do a little more work. Remember that the main purpose of the controller is to initialise scope object, use the controller to initialise it, and then check that it does what we expect it to do.
We do this in a beforeEach block. This will build a new $scope object before each test is executed:
describe("MyController", function () {
// Load the module containing MyController
beforeEach(module("app"));
var scope;
// inject the $controller and $rootScope services
beforeEach(inject(function ($controller, $rootScope) {
// Create a new scope from $rootScope
scope = $rootScope.$new();
// Instantiate the controller
$controller("myController", {
$scope: scope,
});
}));
it("should have a hamster", function () {
expect(scope.hamster).toBeDefined();
});
it("should say hello to Hammy", function () {
scope.sayHello();
expect(scope.greeting).toBe("Hello Hammy");
});
});
Exercise - destroy the helidrones
Your helidrones have been destroyed by United Nations special forces.
The calculator app has a reset button which resets the inputs to zero. Write a test which calls the zero() function and checks that num1 and num2 have become zero.
Repeat and Filter
If we have an array of objects in $scope, like this:
.controller('catController', function() {
$scope.cats = [
{name: "Tigger"},
{name: "Cheeze Wizz"}
]
})
we might want to iterate over them and output them one by one. We can achieve this in the template using the ng-repeat directive:
<li ng-repeat="cat in cats">{{cat.name}}</li>
What is happening here?
For each item in the cats array, the ng-repeat directive creates a new $scope, seeding it with a value ‘cat’. It then compiles the
Search and sort
We can pipe our cats array through a filters to modify it. ng-repeat will iterate over the modified array
<input ng-model="filterString" />
<input ng-model="orderString" />
<li ng-repeat="cat in cats | filter:filterString | orderBy:orderString">
{{cat.name}}
</li>
You may wish to briefly review the ng-repeat docs here: https://docs.angularjs.org/api/ng/directive/ngRepeat. Familiarise yourself with the available variables.
Exercise - List your Assets
As an evil genius, you will have a list of henchmen that you rely on. Create such a list in JSON and add it to $scope from inside your controller.
Now use ng-repeat to loop over the array and output it to the DOM. Add sort and search to help you choose the right villain for the job.
n.b. If you have a particular love for another domain, please feel free to use that instead.
Exercise - Search the henchmen
We are now going to add a search field.
Create an input field and bind it to a “search” model, like this:
<input ng-model="search" />
Add a filter to allow you to search according to the term.
<div ng-repeat="cat in cats | filter:search"></div>
The filter documentation is here: https://docs.angularjs.org/api/ng/filter/filter
Further Exercise - Sort the henchmen
You have a problem. You have to kidnap a rocket scientist and you need to work out the best henchman for the job. We need to sort the henchmen
Pipe the array through the orderBy filter to sort the array sensibly.
<div ng-repeat="cat in cats | filter:search | orderBy:order"></div>
Review the docs here to find out how: https://docs.angularjs.org/api/ng/filter/orderBy
Optional extension
Add buttons at the top of your page which set the value of order. You can click them to sort the array on different fields.
Further extension
You will find there is a variable called index variable to show the current position in the sorted array.
Exercise - Write Unit Tests
Write a unit test with Karma. All this needs to do is instantiate the controller and check that scope contains the array of objects.
You’ll know that:
- Your controller can be instantiated, and
- Your scope contains henchmen.
Extra Exercise (if you finish first)
Review the track by documentation here: https://docs.angularjs.org/api/ng/directive/ngRepeat#tracking-and-duplicates
Add in a track by clause to your repeater.
Implement the strict search example in the filter docs: https://docs.angularjs.org/api/ng/filter/filter
Networking
We access remote services using the $http service, which we can inject into our controller.
var url = "http://www.mycats.com/api/cats";
$http.get(url);
This gives us a Promise
object, which we can chain callbacks onto. In the callback, we generally push the response in to $scope, then allow the template to take over.
$http.get(url).then(function (response) {
$scope.cats = response.data;
});
Once your data is in scope, a digest will fire and your template will automatically update to display it.
Exercise - update the Henchmen to pull from a JSON feed
- Have a look through the http>
- Refactor your repeat and filter exercise to pull data from a JSON file via AJAX. Remember, your JSON must be well formed to work which means double quotes around all the keys.
JSONP
JSONP is a hack which lets us make cross site AJAX requests. We use the $http object to make a request. We use the .then to respond when an AJAX request completes.
angular
.module("app", [])
.controller("ajaxController", function ($scope, $http) {
var url = "http://example.com/cats?callback=JSON_CALLBACK";
$http.jsonp(url).then(function (response) {
$scope.cats = response.data;
});
});
Choose one of the following exercises:
Flickr Exercise
The Flickr JSONP endpoint looks like this:
Pull the Flickr feed from the Internet and display the images.
Extension
Allow the user to search for a tag.
The Weather - Exercise
You can pull in data from all over the internet. Here’s the JSONP url for Open Weather Map:
http://api.openweathermap.org/data/2.5/weather?q=London,uk&callback=JSON_CALLBACK
Use Open Weather Map to get the weather for a specific location
Extension
Allow the user to search for a city. You can find the Open Weather Map API here:
Templates, ng-include and the $templateCache
Our applications thus far have used a single template. This is fine while we are learning, but will become limiting fairly quickly. We really need to be able to break our application down into smaller templates that can be inserted into the page when they are needed.
The $templateCache
Angular maintains an object called the $templateCache where all the templates for the application are stored. Each template is identified by a unique string (typically a url), and we can pull the data from the template cache using this string.
Putting Data into the $templateCache
We have several options
- AJAX in the template from a URL (this is the default).
- Create an inline template using
<script type="ng-template">
. - Put it directly into the cache using
$templateCache.put
. - Write a directive or a component that defines a template literal.
Requesting data from the $templateCache
We request an object from the $templateCache using a string. This is typically a URL. The cache will first check to see if it already has the template. If not it will use the string as a URL and try to AJAX it in.
ng-include
The simplest way to show a template is by using the ng-include directive:
<div ng-include="/path/to/template.html"></div>
This directive will request a template from the $templateCache. The cache will attempt to supply the template, and will try to AJAX it in if necessary.
<section class="exercise"></section>
Exercise - Create a Flickr Item Template
In the AJAX section we wrote an application to display a Flickr feed. We iterated over the items in the items array and rendered them. In this section we are going to start to break down this app into templates.
First create a template to render a single item in the feed. Now in your ng-repeat loop, render the template.
Note: Because the $templateCache uses AJAX, you will need to be running a server here, or you will get errors when making the AJAX call
Extension
Try using an inline template in a script tag.
Writing to the $templateCache directly
When a template is downloaded, it is saved to the template cache so it available again quickly. It’s possible to write directly to the template cache. This allows you to pre-compile portions of your app into JavaScript, making your app more responsive at the expense of a longer initial load time.
We do this in a run block like this. Run blocks are executed once when the app is first initialised:
angular.module("templates", []).run(function ($templateCache) {
$templateCache.put("cachedTemplate", "<h1>Lovely caching!</h1>");
});
Exercise - Writing directly to the template cache.
Modify the Flickr exercise above, so that the item template is written directly to the templateCache.put`
Animation with transitions
Upshot
- To make $ngAnimate work, you must download the ng_animate.js module from http://angularjs.org. Include it in your main app module to make it available.
- The ng_animate service automatically hooks into any CSS elements animations or transitions applied to the element. It causes enter end leave events to happen on a timeout, allowing you to apply CSS effects.
Transitions
Transitions provide a way to move smoothly from one state to another. For example, a hover effect could be applied slowly giving a pleasing fade.
Transitions are cut down animations. You can’t loop them, or set keyframes. They are simple, easy to apply, and work nicely.
To make an attribute transition smoothly from one state to another, use the transition property:
a {
transition: all 2s linear;
}
You can animate all properties, or just a specific property. You can also specify a timing function.
a {
transition: color 2s ease;
}
Choose from the following timing functions:
- ease
- linear
- ease-in
- ease-out
- ease-in-out
Animatable properties
Transitions don’t work with all properties, but do with most of the ones you would care about. A full list of animatable properties (for Firefox) can be found here
Angular Exercise
We’re going to extend our Flickr exercise from before by adding animations
- Download the correct ng_animate module for your version of Angular from here: https://code.angularjs.org/
- Include the module in your application module
- Modify your flickr exercise so that the images animate in smoothly
Further Exercise
Stagger the animation using ng-enter-stagger and ng-leave-stagger
Full Keyframe Animation
Animation goes further than simple transitions by adding full keyframe based animation to CSS. This has several advantages, particularly in terms of performance. Because the browser is in charge of the animation it can optimise performance in a variety of ways, taking full advantage of the hardware.
Declaring Keyframes
To make this work, we must first declare the keyframes we want to use, like so:
@keyframes pop {
from {
font-size: 100%;
}
to {
font-size: 500%;
}
}
Adding our Animation
Once we have declared our animation, we can add it to the page like so:
h1 {
animation-name: pop;
animation-duration: 3s;
}
Declaring more keyframes
We can add in additional keyframes using percentages:
@keyframes pop {
from {
font-size: 100%;
}
50% {
font-size: 1000%;
}
to {
font-size: 500%;
}
}
Here, we have added another keyframe at 50% of the way through the animation.
Exercise
Extend the Flickr search exercise.
Use Animation to make your images pop onto the page dramatically.
Optionally Make use of ng-enter-stagger to boost the effect.
Filter Exercises
Upshot
- Filters are provided for us by little factory functions which we define.
- Define a filter using myApp.filter(‘filterName’, function() {});
- the function is a factory which should return another function which acts as the filter.
For example
myApp.filter("filterName", function () {
return function (str) {
return "hello there " + str + "!";
};
});
Exercise - Reverse Filter
You can reverse a string using something like:
"abc".split("").reverse().join("");
You might like to try this out in a console.
Create a reverse filter that reverses a string.
Exercise - Filtering the first element from an array
We can filter an array in a similar way. The filter returns a function which receives an array, and returns a modified array.
Create a filter that returns only the first element in the array.
Exercise - Pagination
Create a filter that returns the first n elements from an array.
Exercise - Tweets
Tweets often contain #hashtags and @mentions. Write a a filter that takes a raw tweet and hyperlinks the mentions and hashtags.
Something like this:
I'm sorry @dave, #icantdothat
becomes
I'm sorry <a href="http://twitter.com/@dave"> @dave </a>,
<a href="http://twitter.com/search?query=#icantdothat"> #icantdothat </a>
Exercise - Redit style vote filtering
Given an array of comments and votes like this:
[
{
comment: "I really like cheese",
votes: 10,
},
{
comment: "I'm not so sure about edam though",
votes: -2,
},
{
comment: "Gouda properly rocks!",
votes: 4,
},
{
comment: "I quite like a bit of mild cheddar",
votes: -19,
},
{
comment: "Cheese is just old milk",
votes: -8,
},
];
Create a vote filter that only shows comments which scored over a certain amount.
Hint - use the Array filter method: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
Now add a number field and bind it to a value. When you change the value, only comments with a higher value will be shown.
for bonus points, use a range slider like so:
<input type="range" min="-100" max="100" />
Services & Factories
Services and Factories are objects Angular injectables that we use to create objects. They are very similar to one another and are often used interchangeably.
- Service and factories both yield singleton objects.
- Once created this object will persist and you will get the same object back each time you request it.
- We can inject services and factories into controllers using DI.
A Service
We compose a service by giving Angular a newable function (one which writes to “this”).
angular.module("app").service("helloService", function () {
this.sayHello = function () {
alert("Hello");
};
});
A Factory
A factory on the other hand composes an object and returns it:
angular.module("app").service("helloFactory", function () {
return {
sayHello: function () {
alert("Hello");
},
};
});
Making use of factories and services
Once we declare them, services and factories become injectable. Let’s inject them into a controller:
angular
.module("app")
.controller("myController", function (helloService, helloFactory) {
helloService.sayHello();
helloFactory.sayHello();
});
Services as API accessors
Services are often used as API accessors to take API code out of a controller and keep the controller focussed on $scope.
Here is a service that can talk to Github:
angular.module("app", []).service("github", function ($http) {
var url = "https://api.github.com/events?callback=JSON_CALLBACK";
this.getEvents = function () {
return $http.jsonp(url);
};
});
Further Reading
For more on the technical and conceptual differences between services and factories, read my Stack Overflow post here:
http://stackoverflow.com/a/27545899/687677
Flickr Service
We are going to refactor our Flickr exercise to use a service. If you didn’t complete the Flickr exercise, you can download some sample code from here (look in the exercise section)
- Create a Flickr service, that encapsulates the AJAX logic needed to pull data from the Flickr API. Return the $http object so that we can apply callbacks. You can use the code above as a template.
- Return to your Flickr exercise. Inject the Flickr service into your controller and use this instead.
Further Exercise - Simple shopping cart service
- Create a service to manage a shopping cart. Give it a cart variable which holds an array of objects.
- Add an addToCart function to the service which pushes an item into the cart.
- Write a controller to display the cart. Inject the cart into your controller.
- In your controller, write a simple method on $scope which calls shoppingCart.addToCart.
- In your controller, create an attribute of $scope that holds the shoppingCart.cart.
- Write a view to render the cart.
Downloads
Promises
Promises are now a core feature of JavaScript, and are available in Chrome and Firefox. This is interesting news.
Angular gives us $q which is an implementation of promises that works in all modern browsers including IE.
What is a promise?
JavaScript is single threaded, but it needs to deal with asynchronous events. In the past we used callbacks for this. Say we wanted to make an AJAX request, we might pass a callback in like this:
var url = "/mydata.json";
$.getJSON(url, function (data, status) {
if (status == "200") {
// update the page;
} else {
// alert the user
}
});
This is fine if we need to do only one thing, but what if we need multiple things to wait for each other? Now we have callbacks within callbacks within callbacks. Our code becomes a callback Christmas tree. A deeply nested triangle of code decorated with behaviour.
More recently we got the ability to write code like this (again jQuery):
var url = "/mydata.json";
$.getJSON(url)
.done(function (data) {
// update the page;
})
.fail(function () {
// alert the user
});
This is a nicer pattern. We have smaller functions, each of which does a single thing, and the conditional is gone.
The call to $.getJSON(url) returns a promise of future data, a Promise. We can chain methods off it to deal with the success and error states.
Promises are a formalisation of this syntax. We get standardised method names and parameters, which makes it easier to make libraries interoperate with one another.
Promises are now core JavaScript
A variety of Promise specifications and libraries have been produced over the last few years. Fortunately we now have an official specification.
We create a promise like this:
var getA = new Promise(function (resolve) {
var a = 12;
resolve(a);
});
We can then have something happen when the promise resolves:
getA.then(function (a) {
console.log(a);
});
Chaining thens
We may wish to have multiple things happen when our promise resolves. If our thenned function returns a value, that value will be passed on down the chain, like so:
var getA = new Promise(function (resolve) {
var a = 12;
resolve(a);
});
getA
.then(function (a) {
console.log(a);
return a + 1;
})
.then(function (b) {
console.log(b);
return b + 1;
})
.then(function (c) {
console.log(c);
});
If the function returns a value, the next element in the chain is called right away, but if a thenned function returns a promise, we wait until it is resolved. This can happen anywhere in the chain.
var getA = new Promise(function (resolve) {
var a = 12;
resolve(a);
});
getA
.then(function (a) {
console.log(a);
return a + 1;
})
.then(function (b) {
console.log(b);
// This promise won't resolve for 1000ms
return new Promise(function (resolve) {
setTimeout(function () {
resolve(b + 1);
}, 1000);
});
})
.then(function (c) {
console.log(c);
});
Creating a promise which has already resolved
This code is not cool:
var getA = new Promise(function(resolve) { resolve(); });
I’ve done this solely for the purpose of creating a promise object which I can chain from. We can create a promise which is already resolved like so:
var sequence = Promise.resolve();
When a promise is in the resolved state, any thennable functions fill be executed right away in sequence.
sequence
.then(function () {
console.log("First");
})
.then(function () {
console.log("Second");
});
We can then make use of this promise to chain sequential code.
Thenning regular functions
This means that any function which receives a value and returns one can be integrated into the chain. For example parseInt:
var getA = new Promise(function (resolve) {
var a = "12";
console.log(a + a); // outputs "1212"
resolve(a);
});
getA.then(parseInt).then(function (a) {
console.log(a + a); // outputs 24
});
Catch
It may be the case that our promise fails to resolve. Perhaps our AJAX request fails. Perhaps our API is down. Perhaps there is no database. Perhaps we timeout.
In these instances we might need to reject the promise. We can do this like so:
var getA = new Promise(function (resolve, reject) {
reject("Sorry, there was no A");
});
We can then catch the error like so:
getA.catch(function (reason) {
console.log(reason);
});
If a promise is rejected, we skip any intervening thens, and go right to the catch, then we continue as normal.
var getA = new Promise(function (resolve) {
resolve();
});
getA
.then(function () {
console.log("one");
return new Promise(function (resolve, reject) {
reject("There was a problem!");
});
})
.then(function () {
console.log("two");
// This function will not be called
})
.then(function () {
console.log("three");
// Nor will this one
})
.catch(function (error) {
// We skip right to this one
console.log(error);
})
.then(function () {
console.log("four");
// This function will be run
});
Note that if your code throws an exception, the promise will be implicitly rejected and execution will proceed to the next catch block.
Promise.all
Sometimes we might want to wait until all our promises have resolved before we execute the next step. For this we have Promise.all.
var waitAWhile = new Promise(function (resolve) {
setTimeout(function () {
console.log("waited a while");
resolve("a");
}, 1000);
});
var waitAWhileMore = new Promise(function (resolve) {
setTimeout(function () {
console.log("waited a while more");
resolve("b");
}, 2000);
});
Promise.all([waitAWhile, waitAWhileMore]).then(function (a) {
console.log(a, "done waiting"); // output ['a', 'b'] 'done waiting'
});
The thenned function will receive an array of return values from the functions.
Promises in Angular with $q
We can use native promises in Angular, but we might be more happy injecting the $q service into our application.
We can create a $q promise like so:
$q(function (resolve, reject) {});
Say we have an API accessor service, typically we might just return the $http promise object like so:
angular
.module("github", [])
.constant("githubBase", "https://api.github.com")
.service("github", function ($http, githubBase, $q) {
this.getEvents = function () {
var url = githubBase + "/events?callback=JSON_CALLBACK";
return $http.jsonp(url);
};
});
But what if we wanted to pre-process the result, we could compose a promise which does more work, like so:
angular
.module("github", [])
.constant("githubBase", "https://api.github.com")
.service("github", function ($http, githubBase, $q) {
this.getEvents = function () {
var url = githubBase + "/events?callback=JSON_CALLBACK";
return $http.jsonp(url).then(function (response) {
resolve(response.data.data);
});
};
});
Now we might use this in our controller:
angular
.module("app", ["github"])
.controller("demoController", function ($scope, github) {
var showSpinner = function () {
$scope.spinner = true;
};
var hideSpinner = function () {
$scope.spinner = false;
};
var showError = function () {
$scope.error = true;
};
showSpinner();
github
.getEvents()
.then(function (github) {
$scope.github = github;
})
.catch(showError)
.then(hideSpinner);
});
Exercise - Make use of promises to show a spinner
Extend your Flickr controller. Use promises to show and hide a spinner.
Use a catch to show an error if the feed fails to download.
Exercise - Preprocess your flickr response
Modify your Flickr service to preprocess the response and only return the list of items.
Directives
Angular is a DOM compiler. It receives an HTML template, and compiles it into an app. Directives allow us to extend the capabilities of the compiler. We can recognize new elements, new attributes, even classes.
Directives let us touch the DOM
Directives are the places where Angular is wired into the DOM. In a directive, we get full access a particular DOM node including the element, element children and attributes.
Try not to touch the DOM anywhere else (except in templates). If you find yourself needing to do this, you may need to have a rethink.
Built-in directives
Angular comes with many built-in directives which we have been using. Some obvious examples are:
- ng-repeat
- ng-model
- ng-show
- ng-app
- ng-src
Some less obvious examples are:
- form
- input
- textarea
Angular will treat form elements, input elements, hyperlinks, img tags and many others as directives.
Don’t Despise the Built-In Directives
You can get quite a lot of your work done using the built in directives. Upon finding a new hammer, it’s normal to find that everything starts looking like a nail. Before you get stuck into creating a directive consider if you really need one or if the built-in directives will do the job.
Declaring a directive
We declare a directive by writing a constructor function that returns a definition object, like so:
angular.module("app", []).directive("usefulDirective", function () {
var directive = {
template: "<p>Hello from the directive!</p>",
restrict: "A",
scope: true,
controller: function ($scope) {},
};
return directive;
});
- template - a string that will be used as a template. This is a real template and can include other directives or curly braces.
- templateUrl - use this instead of template if your template content is web accessible.
- restrict - Accepts the characters “AEC” or any subset thereof. Tells the directive to match an attribute, element or class.
- scope - pass true if you want a new child scope.
- controller - your directive can have a controller which can receive injectors in the normal way.
Use your directive like this:
<div useful-directive></div>
Directive naming - HTML is case InSenSiTiVe!
Because HTML is not case sensitive, directive names are camelCased in the JavaScript and hyphenated-in-the-html. Angular automatically translates between the two forms.
This is very important to remember. This will bite you if you forget it.
- In your HTML, hyphenate-your-directive-names
- In your JavaScript, camelCaseYourDirectiveNames
Precomposing templates
It’s common to use Gulp or Grunt to compose your template strings, perhaps from a directory of HTML.
Precomposing your templates will limit the number of AJAX requests your app needs to make to get hold of the content.
See the section on Gulp if you’d like to know more on this.
Exercise - Flickr Directive
We are going to create a directive which displays the Flickr application.
- Review the Angular directives guide - https://docs.angularjs.org/guide/directive
- Create a directive which has a controller and a template. The controller should get the flickr feed (ideally using a service). It should save the feed into $scope, so the template can display it.
- It should be possible to add a
<flickr></flickr>
element to the page, and have it output a little flickr application, possibly containing a list of cat pictures.
<flickr></flickr>
Don’t worry about passing data into your directive at this stage, we’ll get to that.
Directive Compilation and the link function
Every DOM manipulation we have made so far has involved templates and $scope. For almost every case, this is sufficient.
However, sometimes we do need just a little more control, and so Angular gives us direct, low level access to the compiler.
Link
The link function is where we can make direct DOM manipulations, access attributes, and pretty much do whatever we like.
Misko has called it the escape hatch, you can do everything here, but before you do so, consider if you need it. Often there is a solution involving templates that will be simpler and more maintainable.
Here is a link function that appends a string to the current element.
angular.module('app', [])
.directive('usefulDirective', function() {
var directive = {
link: function(scope, element, attributes, controller) {
element.append('Hello Angular!')
}
}
return directive;
});
Link Function Parameters
This function is low level and doesn’t work with injection. It receives hardcoded parameters.
The parameters are:
- scope - the current directive scope. Note this will be more complex if you have a transclusion scope active, more on this later.
- element - the element that your directive has been applied to. This is a jqLite element, you can talk to it just like jQuery.
- attributes - the element attributes. You can optionally use this to pass values to your directive. You can also pass values using an isolate $scope. More on this later.
- controller - the current controller object
JQLite or templates
Directives give us access to jQuery or jqLite. If jQuery is available, Angular will automatically make it available. If not, Angular will use jQLite which is a cut-down version of jQuery.
However in most cases you will find that you can get your work done faster and more cleanly using templates, and this is the approach you should generally favour.
Simple Exercise - a link function
Create a very simple directive using the code above that uses the link function to append the string “hello from the directive” to your directive using jqLite.
Simple Exercise - accessing attributes
Create a simple greeting directive using the code above. Add an attribute “name” to your element. The directive should look in the attributes array and append ‘hello dave’ to the DOM, assuming the name was “dave”.
Order of Compilation
Angular is a real compiler which will traverse your DOM executing directives as it goes. Understanding the order of compilation is crucial to understanding directives.
Angular will traverse your DOM depth first, instantiating controllers on the way down, and running link functions on the way up.
- If your directive has a controller it will instantiate this on the way down the DOM tree.
- If your directive has a link function it will execute this on the way back up the DOM tree after all the controllers have been instantiated. We guarantee the existence of all controllers before the link functions are run.
angular.module('app', [])
.directive('usefulDirective', function() {
let directive = {
controller: function($scope) {
// this will be instantiated on the way down the tree
},
link: function() {
// this will be instantiated on the way up the tree
}
}
return directive;
});
If you want to execute link functions on the way down the tree declare pre and post link functions like this:
angular.module('app', [])
.directive('usefulDirective', function() {
var directive = {
controller: function($scope) {
// this will be instantiated on the way down
},
link: {
pre: function() {
// this will be instantiated on the way down
// but after the controller
},
post: function() {
// this will be instantiated on the way up
}
}
return directive;
});
Exercise - Parameterise Flickr
We’re going to use the link function to access a attribute from the tag.
Assume we have a directive which we would like to use like this:
<div flickr tag="toast"></div>
Add a link function to the directive that will look inside the attrs array and pull out the value for tag. Save this value in $scope.
Now in your controller, watch the tag attribute, and get the feed when it is set, something like this: $scope.$watch('tag', getFeed)
Remember your controller will be instantiated before the link function is run.
Bonus Exercise - Random quote
Create a directive which renders a random quote on the page. Use the link function to replace the content of the current element with the joke.
Bonus, pull the quote from an API, such as the Chuck Norris random joke API: http://api.icndb.com/jokes/random
Isolate Scopes
When we create a directive we often (but not always) want it to act like it’s own little application, separate from its parent. In short, we want it to have its own $scope.
Angular allows us to do this. We can create a directive which has an isolate scope. We can optionally pass parameters to the child scope, seeding it, and we can also optionally bind values from the document scope, so that when the document scope updates, those parameters in the child scope also update.
Creating an isolate $scope
Creating an isolate scope to be an empty object, like so:
.directive('myDirective', function() {
return {
scope: {}
}
})
This will create a little application completely divorced from its parent.
Passing parameters to an isolate $scope with @
We can parameterise our directive using @. We can tell our directive to take an initial value from an attribute of the directive element, like so:
.directive('myDirective', function() {
return {
scope: {cheese: '@'},
template: "<input ng-model='cheese' />{{cheese}}"
}
})
The isolate $scope will be seeded with the cheese attribute from the directive element.
<div my-directive cheese="Wensleydale"></div>
Two-way isolate $scope binding with =
@ will allow you to pass a variable into your isolate scope.
= will allow you to bind a value in the isolate scope to a value outside of the isolate scope.
This works using simple watchers. Watchers are created to monitor the values on both $scopes. When one changes, the watcher will update the other and vice versa.
When to use isolate scopes
Isolate scopes should be used with care. They are not suitable for every case as they break the simple $scope inheritance tree. It’s worth noting that none of the built in Angular directives use isolate scopes.
Use an isolate scope when you have created a reusable component that you might want to use in multiple places, for example, a map, or a login form component.
While isolate scopes give us portability they do this at the expense of some flexibility.
Isolate scopes with transclusion
Isolate scopes will typically be used with a template to work properly. Only template code will gain access to the isolate scope. Transcluded content (content nested in the directive) will still use the parent scope.
Exercise - Isolate the Flickr app
Give your Flickr app an isolate scope. It should be able to receive a tag from its parent scope.
I want to be able to call it like this:
<input ng-model="search" /> <flickr tag="search"></flickr>
Transclusion
Transclusion allows you to take content that is already in the element and modify it in some way. Say you have a div like this:
<div my-directive>World of Wonder</div>
Now you apply a directive which includes a template.
.directive('myDirective', function() {
return {
template: "<div>World of Template</div>"
}
});
The innerHtml is set on the div and “World of Wonder” is no more. Sad. This is where transclusion comes in.
Transclusion allows us to access the element’s original content which has been displaced by the template
When we tell a directive to transclude, the original content of the element is saved. Why would you want to do this?
Uses of transclusion
- ng-if - the transcluded content is only visible when a condition is met.
- ng-repeat - the transcluded content is repeated for each element of an array.
- wrapping an element - for example, wrapping a row of buttons in a menu bar.
Scope of transcluded content
Transcluded content will have the scope, the transcluded content will not share it unless you explicitly compile against the isolate scope.
Consider carefully whether you need an isolate scope in this case.
Two ways to transclude
1. the transclude directive
If we tell our directive to transclude, the content that was originally in the element will still be available to us via the transclude directive in our template, like so:
myApp.directive("transclusionDirective", function () {
return {
transclude: true,
template:
"Here is the transcluded content: <span ng-transclude></span> Nice huh?",
};
});
2. Use the transclude function in the link function
If you have specified transclude: true, The link function will receive a handy transclusion function as it’s 5th parameter. Call this function to compile the transclusion. Optionally pass in a scope to compile it with a scope object.
This will allow you to compile the transcluded content against the isolate scope. Beware, this might surprise your user as you will have broken the $scope inheritance tree in your HTML. Do this only occasionally and with proper thought.
myApp.directive("transclusionDirective", function ($compile) {
return {
transclude: true,
link: function (scope, element, attrs, controller, transclusion) {
transclusion(scope, function (compiledTransclusion) {
element.prepend(compiledTransclusion);
});
},
};
});
Exercise - A transcluded header directive
Create a directive that will add a header and footer to any element. You can do this entirely using the a template and the ng-transclude directive.
Exercise - repeater
Implement a really dumb little directive that simply repeats the content 5 times.
Bonus marks for making it configurable, so that we can repeat the content n times. You would do this by inspecting the attrs array in the pre-link function.
Bonus points for using the $parse service to parse the transcluded template as Angular code.
Use the attr parameter in your link function to receive the value.
Call it like this:
<div repeat="5">Hey there!</div>
Exercise - ng-if
Reimplement ng-if. The transcluded content is shown if the passed in expression evaluates to true. You will need to use [parse) to evaluate the passed in expression.
You will not need an isolate $scope here.
Call it like this:
<input ng-model="val" type="checkbox" />
<div if="val">Hey there!</div>
CRUD
The simple API provides an API which allows you to list, view and manipulate Articles, Authors and Comments. It’s a fully functional CRUD API built on Rails that get’s flushed each night.
Exercise - Create a service to access the articles
- Create a service which can access the article api, doing a get request for the list of articles.
- Make a controller to call the service and get the articles and add them to $scope.
- Write a template which will display all of the articles.
Exercise - Create articles
Extend your service so it can post to the articles API to create a new article. Call it manually from within the controller to test it works.
Create form template and ng-include it on your page. Create a controller to manage form submission. Optionally add a little link or button to show and hide the form. Create a new article object in your controller and bind the form fields to it.
Now in your controller, write a submit function. Cass this function with ng-submit. This function should send the new article to your service and save it.
Hard Exercise - Write a transcluded directive to add edit links
Transclusion allows us to wrap the content of an element inside a template.
Write a transcluded directive which adds edit links to your articles. When you click it, it should make the content editable in some way by revealing a form. If possible, reuse the article form template you wrote before.
Form Validation
The form tag is an Angular directive. When you create a form on the page, Angular will create a new scope. If you give your form a name attribute, Angular will add a variable to your new scope with that name which contains validation info.
Form Exercise
Either:
- Extend the CRUD exercise if you have done this or…
- Extend the profile form exercise from day one.
- Create a form tag. Give it a name attribute. Turn off browser validation by adding the novalidate property.
- Give each of the input elements a name and bind them to scope using ng-model.
- Now use curly braces to output a value with the same name as the form.
Something like the following:
<form name="loginForm" novalidate>
<input name="email" ng-model="email" />
<input name="password" ng-model="password" />
<pre>{{loginForm | json}}</pre>
</form>
Use curly braces to output a scope attribute with the same name as your form and see what Angular has given you.
Validation
Each input gets an attribute in the loginForm object. There is an $error object for the form and each input gets its own error object as well.
We use html5 form validation attributes to tell the form how to validate, for example:
- type=“email”
- type=“number”
- required
You can also use pattern to match against an arbitrary regex:
- pattern=“https?://.+”
We also get some custom validation directives from Angular:
- ng-min-length
- ng-max-length
Exercise - adding validation
Add sensible validation to your form. Use an ng-if to show an error if any fields are invalid.
If you are extending your CRUD exercise, modify your submit method so that it won’t submit the form unless the form is valid.
Styling forms
Each input gets some new classes: ng-dirty is set when the field has been typed in. Don’t highlight any error fields until ng-dirty is set. ng-invalid is set when the field is invalid.
Exercise - Style invalid fields
Use a combination of ng-dirty and ng-invalid to style fields that have invalid values, perhaps with a red border.
Use ng-valid to style fields that have valid values, perhaps with a green border.
Further Front End Exercise - ng-messages
ngMessages is a new module which standardises messages based on key value pairs. Read about it here: https://docs.angularjs.org/api/ngMessages/directive/ngMessages.
You can use ngMessages to display form errors based on the $error object.
Do this now. Display errors at the top of the form.
Optionally display messages above each invalid form element.
Further Hardcore Coder Exercise - Custom validation
We can provide custom form validation with a directive.
The form directive binds a controller to the form. This controller has a method $setValidity. We can get this controller in the link function of a directive as the 4th parameter. Call this method to manually invalidate the form based on the value of an element.
We can get the value of an element using el.val();
Here’s a validator directive that always makes the form be invalid.
angular.module("app", []).directive("myValidator", function () {
return {
require: "ngModel",
link: function (scope, el, attrs, controller) {
scope.$watch(attrs.ngModel, function () {
controller.$setValidity("always invalid", false);
});
},
};
});
Create a custom validator that checks the password against a set of common passwords:
["password", "passw0rd", "mypassword"];
Protractor
Protractor is an end to end test framework for Angular. It gives you a JavaScript enabled web browser (Selenium) and a simple JavaScript SDK that lets you drive it. The tests themselves are typically written in Jasmine.
Protractor is the standard test harness used by the Angular core team. You should consider making use of it in your projects.
Dependencies
Protractor runs as a Node module, so you’ll need Node installed. You’ll also need the Java SDK.
If you are running Windows you will also need the .Net framework.
Browser
Tell your browser to hit specific paths and URLs, like this:
browser.get("http://nicholasjohnson.com/");
Your browser will literally navigate to this address.
Element
Select elements on the page, for example form fields, buttons and links. Call methods to interact with them. For example:
element(by.id("gobutton")).click();
Installation
Follow the instructions here to install protractor and start up webdriver:
http://angular.github.io/protractor/#/
Now create a config file. Save it as protractor.conf.js in your project directory.
exports.config = {
seleniumAddress: "http://localhost:4444/wd/hub",
specs: ["selenium_specs/*.js"],
};
What to test
Protractor is for end to end testing user acceptance testing. The internals of your code are a black box. You don’t need to check the internals of your app, or how a particular result was achieved, you just need to know that a particular end goal has been reached.
Exercise - Create a Test
Now try to create a test suite for your CRUD application. You should be able to drive the browser to create a new article, then navigate to the article and verify it exists.
You can:
- Tell the browser to navigate to a URL.
- Get elements and click on them or type in them.
- Get a single element using element(by.css(‘.some_classname’)) for example and check it’s content with getText().
- Get a list of elements using element.all(by.css(‘.some_classname’)) for example and count it’s length.
$resource
ngResource is a module that provides us with a $resource service. We can use this to make API calls.
We can wrap our resource up in a factory, allowing us to generate a resource that will make an API call on our behalf and instantiate itself.
$scope.author = $resource(
"http://simple-api.herokuapp.com/api/v1/authors/:id"
).get({ id: 1 });
The $scope.author object is in fact a promise. When the promise is fulfilled, the author will instantiate itself. This gives us this rather nice syntax, even though the author is instantiated asynchronously.
Using a factory
It’s common to create resources using a factory, like so:
app.factory("Author", function ($resource) {
return $resource("http://simple-api.herokuapp.com/api/v1/authors/:id");
});
We can now get an author:
var author = Author.get({ id: 1 });
There’s some magic going on here. The author object is in fact a promise that will configure itself when it is satisfied.
Making overrides
We can make overrides to our resource routes by passing in a configuration object. This lets us use custom methods and URLs.
app.factory("Author", function ($resource) {
return $resource(
"http://simple-api/api/vi/authors/:id",
{},
{
update: {
method: "PUT",
},
}
);
});
Reading
Read the API spec here: https://docs.angularjs.org/api/ngResource/service/$resource
Resource Exercise
Extend your CRUD exercise. Create a comment resource and use it to pull comments for articles.
For extra bonus points create a comments directive. You might write:
<comments for="12" />
to get the comments for article 12.
Routes
Routes are configured injecting the $routeProvider object into the config function.
Here’s a simple example:
myApp.config(function ($routeProvider) {
$routeProvider
.when("/home", {
templateUrl: "home.html",
})
.when("/about", {
templateUrl: "about.html",
controller: "aboutController",
});
});
Use the ngView directive to output the content specified by the route:
<div ng-view></div>
Entry Exercise - Create a two page AJAX site
- Read the docs here: https://docs.angularjs.org/api/ngRoute/service/$route
- Create a site with two pages of content, home and contact. Use routing to swap between them.
Exercise - Flickr Links
Add links to the top of your flickr app to allow the user to perform common searches - cats, hamsters, cheeses.
The user should be able to click the link to hit the URL and display the template.
Optionally activate HTML5 mode.
Exercise - Flickr sharable URL
Extend the Flickr app so that the tag is in the URL.
When I click search, navigate to a url that contains the tag, like this:
localhost/flickr.html/#cats
This should show a list of cats.
You may need to use the location.path(’/#cats’);)
Exercise - Simple API
Create a route which displays a single article from the simple API. It should receive an id parameter, then use this to make an api request to the server via your service.
You will need to inject the $routeParams service into your controller to retrieve the parameters in your controller.
From your articles index, link to the individual articles.
Further Exercise - New article route
Create an “article_form” template in a separate file. Create a route that will display the form. The form fields should be bound to the scope. When you submit the form, use the values in scope to post to the API, creating a new article and displaying it.
UI-Router
The default Angular router lets us define URLs and describe how the router should respond, which template it should pull in, and which controller it should use.
UI Router comes from a different angle. Instead of defining URLs, we define named states, then define the URL, controller and templates associated with that state.
We can have multiple named template insertion points for a single state. We can define nested states. We can drop into HTML5 mode and generate real URLs.
There are 3 ways to change state
Using the ui-sref directive
From the template using the ui-sref directive like so:
Using the $state service
From JavaScript using the $state service:
$state.go("statename");
By simply changing the URL
From the browser by navigating to the URL. You can do this by typing in the address bar, or by setting document.location.
A default route
We can create a default route using otherwise:
angular
.module("app", ["ui.router"])
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/");
});
Any unmatched route will redirect to ’/’
A state
We define a state like this:
angular
.module("app", ["ui.router"])
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/");
$stateProvider.state("home", {
url: "/home",
templateUrl: "home.html",
});
});
We use the ui-sref directive to generate hyperlinks based on the state names. We use the ui-view directive to locate our template.
<!DOCTYPE html>
<html>
<head>
<title>Scope</title>
<script src="angular.js"></script>
<script src="ui-router.js"></script>
<script src="demo.js"></script>
</head>
<body ng-app="app">
<a ui-sref="home">home</a>
<div ui-view></div>
</body>
</html>
The state has a name, a URL and a template URL. It can also optionally have a controller.
Exercise - Add a route to your CRUD
Add a router to your CRUD application that can show a particular article based on the URL.
Optionally add a route to show a list of articles with links to navigate.