If you’re planning to upgrade your codebase to Angular 2, there are particular things you can begin to do to start getting into the Angular 2 mindset. In this article, we’ll be going through some things you can do to an existing 1.x codebase to bring it into shape for any future Angular 2 refactoring.
As a side note, even if you are not planning to move to Angular 2 in the near or distance future, getting your Angular 1.x app into the latest best practices state will benefit your development in many many ways, ranging from application maintenance all the way through to writing better JavaScript for the future.
We'll start with ES6 here, or ES2015 for the pedantic. Using ES6 will get your app half-way there in terms of using a "modern" JavaScript tech stack.
You can start converting your ES5 apps across to ES6 with ease, and one file at a time as well. This gives you a lot of breathing room for short bursts of "modernizing" your app(s). Let's compare some ES5 versus ES6 code:
// ES5 version
function TodoController(TodoService) {
var ctrl = this;
ctrl.$onInit = function () {
this.todos = this.todoService.getTodos();
};
ctrl.addTodo = function (event) {
this.todos.unshift({ event.label, id: this.todos.length + 1 });
};
ctrl.completeTodo = function (event) {
this.todos[event.index].complete = true;
};
ctrl.removeTodo = function (event) {
this.todos.splice(event.index, 1);
};
}
angular
.module('app')
.controller('TodoController', TodoController);
The ES5 version uses plain old JavaScript functions - this is great, and perfectly acceptable. However, if you're considering leaping to ES6, a class
may make a lot more sense, as well as being in line with Angular 2's component classes.
The common thing we need to do to the above code is reference the this
keyword of the controller so that we are able to use it across different lexical scopes. I prefer this over Function.prototype.bind
as it's clearer to me what's happening when using the ctrl
namespacing, plus it's a little bit faster.
With that in mind, let's convert the code to ES6:
// ES6
export default class TodoController {
constructor(TodoService) {
this.todoService = TodoService;
}
$onInit() {
this.todos = this.todoService.getTodos();
}
addTodo({ label }) {
this.todos.unshift({ label, id: this.todos.length + 1 });
}
completeTodo({ index }) {
this.todos[index].complete = true;
}
removeTodo({ index }) {
this.todos.splice(index, 1);
}
}
You can see here that we've freed things up from Angular's boilerplate to a standalone piece of business logic for our component. We're using an ES6 class with the $onInit
lifecycle hook as a property on the class. We're also using object destructuring inside the addTodo
, completeTodo
and removeTodo
functions to only fetch the label
or index
property that's returned through the arguments.
So where is the angular.module().controller()
call? If you're using the right patterns with components, you can bind the exported controller
onto the component. This means that it doesn't even need to be registered with the Angular core.
An example:
// todo.component.js
import angular from 'angular';
import controller from 'TodoController';
const todos = {
controller,
template: `
<div>
<todo-form
new-todo="$ctrl.newTodo"
on-add="$ctrl.addTodo($event);">
</todo-form>
<todo-list
todos="$ctrl.todos"
on-complete="$ctrl.completeTodo($event);"
on-delete="$ctrl.removeTodo($event);">
</todo-list>
</div>
`
};
export default todos;
In this example, we're importing just the controller
under the default export, which means that we can call it whatever we want. For ES6 shorthand property setting on the component Object, we can just pass in controller
; this is essentially the same as controller: controller
. For more Angular ES6 patterns, check out my Angular 1.x ES2015 styleguide, updated with component architecture practices.
We can take this one step further and begin to incorporate immutable patterns. So far, we're using "mutable" patterns, which means that we are mutating state. Thinking about immutable operations is a great way to develop with a uni-directional dataflow.
So what is a mutable operation? In the above examples we were doing:
removeTodo({ index }) {
this.todos.splice(index, 1);
}
Using .splice()
will actually mutate the existing Array. This is fine in general practice, but we want to be more intelligent about our mutations and state changes, being careful not to cause any unintended side effects, and think about performance. Libraries like React and frameworks like Angular 2 can actually perform faster Object diffing by seeing what's changed, rather than predicting and re-rendering an entire collection (for example).
This is where we would construct a new collection and bind it instead. In our removeTodo
example, it would look like this:
removeTodo({ todo }) {
this.todos = this.todos.filter(({ id }) => id !== todo.id);
}
In this instance, we're using Array.prototype.filter
to return a new collection of data. This allows us to construct our own dataset using an immutable operation, as .filter()
does/will not mutate the original Array.
From this we construct our new Array, by filtering out the todo that did in fact match the current item being iterated over. Using .filter()
will simply produce false
on this expression, in turn removing it from the new collection. The initial reference to this.todos
has not been changed at this point - we've simply iterated and created a collection based on an expression we provided to fetch all of the todos
that are not being removed.
We can also perform time travel debugging whilst using immutable operations, allowing us to step through state mutations and debug code more easily. There is much more power in controlling what state mutations are made, after which we rebind to this.todos
once we are ready.
A full look at immutable operations would look as follows:
class TodoController {
constructor(TodoService) {
this.todoService = TodoService;
}
$onInit() {
this.todos = this.todoService.getTodos();
}
addTodo({ label }) {
this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos];
}
completeTodo({ todo }) {
this.todos = this.todos.map(
item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item
);
}
removeTodo({ todo }) {
this.todos = this.todos.filter(({ id }) => id !== todo.id);
}
}
This will allow you to, if you deem it necessary, use something like Redux inside Angular 1, and move it to Angular 2 as well. For Angular 2, I'd recommend ngrx/store
as the go-to state management library, for Angular 1, $ngRedux
is of the most popular.
TypeScript is becoming the standard for JavaScript development in Angular 2, whether you like it or not - and for good reasons. Despite some features that deviate to looking like backend languages, TypeScript does make sense. If you are considering using Angular 2, then TypeScript is probably a wise idea for you if you're going to upgrade an existing codebase.
Before we get to Components, let's start with Directives. There was, and still is, much confusion around what the definition of a "Directive" actually is. Is it a template? Does it contain view logic? Does it manipulate the DOM? Does it do all the things and end up messy? Maybe...
To sum up, a Directive is/should:
If you think about ng-repeat
, for example, this is a behavioral directive that reconstructs the DOM based on the data input into it. It does not go ahead and create a bunch of code that you did not ask it to. When you need to write templates that contain view logic, this is where a component comes in.
A Component is/should:
Based on this, the idea is that when you want custom DOM manipulation, which we occasionally need to access the DOM with frameworks, then a Directive is the place for that.
Component architecture is a pretty new concept to the Angular world, and it's been kicking around in React for years. Angular 2 saw an opportunity in React's component-based approach and uni-directional data flow and stood on its shoulders.
When you think and architect your application in a tree of components, rather than thinking about "pages" and "views", dataflow and predictability become much easier to reason with, and, in my experience, you end up writing a lot less code.
Essentially, you'll want to architect in a tree of components, and understand the different flavors of components. Typically we have smart and dumb components, otherwise known as stateful and stateless components. Dan Abramov has written about this in more depth - I urge you to check it out.
Again, when moving away from the views/pages mentality, we should favor component routing. The latest release of ui-router - which you should 100% be using for Angular 1.x applications - not only supports routing to components instead of views, but it also supports Angular 2, and React. It's magical.
An example of a component route:
// referencing the "todos" component we illustrated above
$stateProvider
.state('todos', {
url: '/todos',
component: 'todos',
resolve: {
todos: TodoService => TodoService.getTodos()
}
});
Inside the todos
state, we're using resolve
to fetch todos, rather than inside the controller. This may make more sense for preloading data before you hit that routed component. Interestingly, we can use the todos
property inside resolve
to get that data passed to us as a component binding called todos
:
import angular from 'angular';
import controller from 'TodoController';
const todos = {
bindings: {
todos: '<'
},
controller,
template: `
<div>
<todo-form
new-todo="$ctrl.newTodo"
on-add="$ctrl.addTodo($event);">
</todo-form>
<todo-list
todos="$ctrl.todos"
on-complete="$ctrl.completeTodo($event);"
on-delete="$ctrl.removeTodo($event);">
</todo-list>
</div>
`
};
export default todos;
What is this mystical '<'
syntax? One-way dataflow. Let's explore a little further with a different example.
One-way dataflow is predictable and easier to debug. The idea is that data is passed down, mutated, and then events are passed back up to inform the parent that something needs to change. This concept applies in Angular 1.x components, Angular 2 and also React (however, we are in no way limited to just those three).
Let's assume we want to add a new todo. We have our addTodo
function that accepts an event
Object, but we destructure it to just fetch our label
property:
addTodo({ label }) {
this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos];
}
From this, we are adding the new todo at the beginning of a new Array, and, using the ES6 spread
operator, we're spreading the existing this.todos
Array into the new one, thus creating our new collection with immutable operators. When the this.todos
changes, our binding using <
is passed new data, which then delegates to the <todo-list>
, thus rendering the new reflected change in the Array:
const todoList = {
bindings: {
todos: '<',
onComplete: '&',
onDelete: '&'
},
template: `
<ul>
<li ng-repeat="todo in $ctrl.todos">
<todo
item="todo"
on-change="$ctrl.onComplete($locals);"
on-remove="$ctrl.onDelete($locals);">
</todo>
</li>
</ul>
`
};
The one-way syntax we're using here is against the todos
coming into todoList
. When the parent data changes, it will be reflected down into the child component, forcing a DOM re-render with the new addition.
If you want to learn more, check out the full code demo of the todo lists with one-way dataflow and immutable operations. For more on these practices, you can review my ES6 + Angular 1.5 components styleguide.
Related resources:
Todd Motto (@toddmotto) is a Google Developer Expert from England, UK. He's taught millions of developers world-wide through his blogs, videos, conferences and workshops. He focuses on teaching Angular and JavaScript courses over on Ultimate Courses.