Introduction
The world has changed significantly since the first release of AngularJS in 2009. Web browsers have vastly improved in both speed and capabilities, a new version of ECMAScript (i.e. Javascript) was released this past year known informally as "ES6" (and formally known as "ES2015"), mobile phones have become ubiquitous which has opened up a world of new opportunities (literally), and other frameworks like React and Redux have made large strides towards paving what the future of web development looks like. Put shortly, the Javascript world moves quickly and AngularJS (now typically referred to as Angular 1.x) has lagged behind a bit due to design decisions made almost a decade ago. The Angular team realized this and responded by building "Angular2" - a complete framework for building web and mobile apps that takes the best ideas from Angular 1.x, React, and others while omitting the cruft that come with those other solutions. Despite still being in beta, Angular2 has already received critical acclaim from the developer community and many are greatly anticipating the final release.
However, many developers have written substantial Angular 1.x codebases over the past few years and (due to the massive size of their codebases) will be unable to do a "rewrite" overnight to Angular2 when it comes out. Naturally, this may have left many developers in a bit of a conundrum had the Angular team not addressed this head on with the release of Angular 1.5 in February 2016.
Angular 1.5 is a new version of Angular that introduces Angular2's core paradigms to existing Angular applications, allowing devs to start writing code that will be much easier to port to Angular2 whilst still building features in their existing Angular codebases. This guide will teach you everything you need to know about Angular 1.5 and best practices to ensure you're writing clean & modular code that can be upgraded to Angular2 with ease. The videos in this guidebook will show you real world examples from a production ready application that we built in Angular 1 and then fully ported over to Angular2.
What this course will cover
The new changes in Angular 1.5 and relevant portions of the Angular Style Guide will be the focus of our learning, including but not limited to:
- ES6
class
support for services and controllers - Overview of the most common ES6 features you should be using
- Principles of component based development
- Angular 1.5's new
component
API - Best practices for writing Angular2 compatible code
- Proper codebase structuring
Incorporating relevant parts of ES6
As a rule of thumb, you want to write as much "vanilla javascript" as possible, as this will ensure you aren't depending on too many Angular 1.x specific functionalities (which would therefore make it harder to port over). Angular 1.5's new support for ES6 classes on services and controllers is especially useful in this regard, as Angular2 services and components are based heavily on ES6 classes.
Using ES6 classes for controllers and services
Before we dive into the details, it's worth refreshing our memory that services in Angular are used to share data & functionality between controllers. If you've used Angular before, you've probably used "factories" more than you've used services. The difference between the two is pretty straightforward:
Factories: you create an object, add properties to it, then return that same object. When you pass this service into your controller, those properties on the object can now be accessed in that controller via your factory.
Services: it's instantiated with the ‘new’ keyword. Because of that, you'll add properties to ‘this’ and the service will return ‘this’. When you pass the service into your controller, the properties on ‘this’ can now be accessed in that controller via your service.
Without the nice class
syntax that ES6 provides, most developers opted to just create an object with factories instead of using services. However, with ES6 we can now create classes with ease!
A service in Angular 1.x using ES5 would look like this:
angular.factory('Comments', function($http){
return {
add: function(commentBody){
return $http.post('/api/comments', {comment: { body: commentBody }});
}
};
})
class Comments {
constructor($http){
this._$http = $http;
}
add(commentBody){
return this._$http.post('/api/comments', {comment: { body: commentBody }});
}
}
angular.service('Comments', Comments)
Similarly, using ES5 your typical controller would look something like this:
function CommentFormCtrl(User, Comments){
this.resetCommentForm = function(){
this.commentForm = {
isSubmitting: false,
body: ''
}
}
this.addComment = function(){
Comment.add(this.commentForm).then(this.resetCommentForm)
}
this.resetCommentForm();
this.currentUser = User.current;
}
angular.controller('CommentFormCtrl', CommentFormCtrl)
With ES6, the same controller would look like:
class CommentFormCtrl {
// Inject dependencies
constructor(User, Comments) {
'ngInject';
this._User = User;
this._Comments = Comments
this.currentUser = User.current
// Initialize blank comment form
this.resetCommentForm();
}
resetCommentForm() {
this.commentForm = {
isSubmitting: false,
body: '',
errors: []
}
}
addComment() {
this.commentForm.isSubmitting = true;
this._Comments.add(this.commentForm.body).then(this.resetCommentForm);
}
}
angular.controller('CommentFormCtrl', CommentFormCtrl)
As you saw in the video above, the Angular2 versions had striking similarities with the 1.5 + ES6 classes we wrote. We also introduced some other ES6 functionality that is common in Angular 2 development. If you haven't heard of ES6 yet, it's basically new syntax and features for Javascript. The most commonly used parts in your day-to-day development will likely be import/export, class, let and const, and arrow functions -- we've included some brief explanations on these all below.. There are lots of other changes as well; here's a comprehensive list detailing them.
Modules (Import / Export)
Before ES6, there wasn't a straightforward way to reuse javascript code in different places. The concept of modules wasn't defined in the Javascript specification, so you'd have to rely on third-party implementations like CommonJS or RequireJS in order to transport code around in your application. With ES6, we gain the ability to modularize our code using the import
and export
keywords
Exporting code using ES5 (CommonJS):
// home.controller.js
function HomeCtrl(){
this.title = 'Welcome Home!'
}
module.exports = HomeCtrl;
Exporting code using ES6:
// home.controller.js
function HomeCtrl(){
this.title = 'Welcome Home!'
}
export HomeCtrl;
Importing code using ES5:
// home.js
var HomeCtrl = require('./home.controller')
var Home = {
controller: HomeCtrl,
controllerAs: '$ctrl',
template: '<h1>{{ $ctrl.title }}</h1>'
};
Importing code using ES6:
// home.js
import HomeCtrl from './home.controller'
let Home = {
controller: HomeCtrl,
controllerAs: '$ctrl',
template: '<h1>{{ $ctrl.title }}</h1>'
};
Classes
Classes in ES6 allow us to define classes in a cleaner syntax. Although it still uses prototype-based inheritance under the hood, the new syntax is a lot closer to other class-based languages (like Java or C++)
Class example in ES5:
// Constructor
function Rectangle(height, width){
this.height = height;
this.width = width;
}
// Static method
Rectangle.describe = function(){
return 'All rectangles have four sides and right angles';
};
// Instance method
Rectangle.prototype.isSquare = function(){
return this.height === this.width;
};
var rectangle = new Rectangle(1, 2);
var square = new Rectangle(3, 3);
Rectangle.describe() // All rectangles have four sides and right angles
rectangle.isSquare() // false
rectangle.height // 1
square.isSquare() // true
class Rectangle{
// Constructor
constructor(height, width){
this.height = height;
this.width = width;
}
// Static method
static describe(){
return 'All rectangles have four sides and right angles';
}
// Instance method
isSquare(){
return this.height === this.width;
}
}
let rectangle = new Rectangle(1, 2);
let square = new Rectangle(3, 3);
Rectangle.describe() // All rectangles have four sides and right angles
rectangle.isSquare() // false
rectangle.height // 1
square.isSquare() // true
Let and const
The let
keyword is like the var
keyword for variable declarations, but let
restricts the variable to the current scope, preventing it from accidentally leaking outside of the current scope, or worse, onto the global scope. const
is also scoped in the same way as let
, but the difference is that (you can probably guess from the name of the keyword) it defines a constant. Variables defined with the const
keyword can only be defined once, then they become read-only. Any attempts to change a constant won't persist.
Here's an example demonstrating the const
keyword and the differences between let
and var
:
const PI = 3.14
PI = 3 // oops
console.log(PI) // 3.14
var x = 42
var y = 42
if(true){
let x = 0
var y = 24 // Same y variable from above being referenced!
console.log(x) // 0
console.log(y) // 24
}
console.log(x) // 42
console.log(y) // 24
for(var i = 0; i < 5; i++){}
for(let j = 0; i < 5; i++){}
console.log(i) // 5
console.log(j) // j is not defined
Arrow Functions
Arrow functions are a shorter way to express anonymous functions, along with the perk of being bound to the current scope (a new scope isn't created within the arrow function).
Here's an example of a arrow function taking advantage of lexical scoping:
let delayedLogger = {
delay: 250,
log: function(message){
setTimeout(()=>{
console.log(message + ' (delayed '+this.delay+'ms)')
}, this.delay) // We don't need to `.bind(this)` since
} // the arrow function is lexically bound
}
delayedLogger.log('Hello Jack!') // Hello Jack! (delayed 250ms)
delayedLogger.delay = 500
delayedLogger.log('Hello Jake!') // Hello Jake! (delayed 500ms)
And the ES5 equivalent:
var delayedLogger = {
delay: 250,
log: function(message){
setTimeout(function (){
console.log(message + ' (delayed '+this.delay+'ms)')
}.bind(this), this.delay) // The `.bind(this)` is required in order to access
} // the `delay` property on the parent object since
} // the anonymous function creates a new scope
delayedLogger.log('Hello Jack!') // Hello Jack! (delayed 250ms)
delayedLogger.delay = 500
delayedLogger.log('Hello Jake!') // Hello Jake! (delayed 500ms)
The arrow function syntax can also be shortened depending on your function body and parameters. If the function only has one parameter, you can omit the parenthesis. If your return value is simply an expression, the curly brackets and return
keyword can be omitted. Curly brackets are still required when there are multiple statemens in the function body.
let doubled = [1, 2, 3, 4, 5, 6].map(a => a * 2)
Compartmentalizing application functionality into reusable components
Web development has been trending towards component-based development and web components. Components allow for a clear separation of concerns and allow you to build highly reusable code. Because of this, Angular2 (and other frameworks like React and Ember) has adopted this style of programming.
The template for a webpage built with components would look similar to this:
<home-page>
<app-header />
<scrolling-banner>Welcome to my webpage!</scrolling-banner>
<p>Enter your info below if you want to get in touch!</p>
<contact-form />
<app-footer />
</home-page>
Components are essentialy "custom HTML tags" that we get to define and control the behavior of. Each component only needs to worry about what goes on inside of it, which allows us to minimize the amount of concerns for that component. Adding another component to the page is as easy as adding another HTML tag to the page, making components highly reusable.
Components in Angular 1.5
If you've been using Angular for a while now, you may already be familiar with the concept of directives
, which can be used in a similar fashion to components. In fact, components are actually directives under the hood (the AngularJS docs calls them "a special kind of directive"), preconfigured with more sensible defaults and a cleaner syntax. Since Angular 2 will be heavily component based, this will reduce friction when it comes time to upgrade to Angular 2.
Here's a basic example of a footer component using the component API:
<app-footer entity-name="'Awesome, Inc.'"></app-footer>
myModule.component('appFooter', {
template: '<footer>© {{ $ctrl.getCurrentYear() }} {{ $ctrl.entityName }}</footer>',
bindings: { entityName: '=' },
controller: function() {
this.getCurrentYear = function() {
return new Date.getFullYear();
};
}
});
You can read more about the component method and how it works from the announcement blog post for Angular 1.5.
Breaking syntax changes between AngularJS and Angular2
With controllerAs, you're allowed to choose the scope's variable name -- "vm" (short for "view model") is/was a popular choice. However, we think that $ctrl is a better choice for two reasons:
- It's the default controllerAs name for the new component API (which we'll cover in a minute)
- It looks visually similar to $scope
While initially it may be tempting to use controller specific names (i.e. 'LoginVM', etc), there's no benefit in using different controllerAs variable names across your application. In fact, it will make it harder to upgrade to Angular2 as you'll likely want to do a simple find & remove for all controllerAs variable instances considering Angular2 templates don't require a reference variable.
Most other syntax changes will be easier to mitigate by doing a simple find and replace -- you can view a comprehensive list of the syntax changes between Angular and Angular2 on the Angular2 documentation site.
Best practices for structuring your codebases
It may be tempting to organize the files in your codebase by type, having a separate folder for controllers, services, templates, etc. since a lot of other MVC frameworks are set up in this way. This can get harder to manage as your application grows since you have may have to look in multiple folders in order to entirely grok a feature. A more intuitive way to organize your files is by feature, keeping all files the files for each feature in their own folder.
src/
|--auth/
|----auth.config.js
|----auth.controller.js
|----auth.html
|--config/
|----app.config.js
|----app.constants.js
|----app.run.js
|--home/
|----home.config.js
|----home.controller.js
|----home.html
|--app.js
index.html
While it may seem weird at first having different types of files in the same folder, this minimizes the number of folders you'll have to look in to work on a feature, and makes even more sense when using components.
Where to go next
If you want to go through the process of creating a codebase that follows these guidelines step by step, we highly recommend checking out the Build an Angular 1.5 application with ES6 and Components course. We also have an upcoming course that will show you how to recreate the Angular 1.5 app from that course in Angular2 by reusing the core ES6 based controllers and services. To stay in the loop, make sure you're subscribed to our newsletter (right below this section) and follow us on Twitter @GoThinkster.