Preparing Your Angular 1 Code For Angular 2 (Part 1)
This post is Part 1 of a series of blog posts where I’ll convert an Angular 1.x project to Angular 2.
Check out Part 2 to see how we update the NodeJS server to use ES2015 code.
Angular.js has changed a lot over the past few years. As the framework has matured, not only have new features been added, but the style in which we write our code has changed dramatically.
We no longer write our Angular code in one giant file or in a couple large files with names like controllers.js
or services.js
. As the framework has matured, conventions have been developed that make it easier for you to jump into another developer’s code and figure out where everything lives.
How Does A Style Guide Make The Angular 2 Conversion Easier?
I’m a big fan of style guides that layout these conventions. I may not agree with all of the conventions of a style guide, but they’re great for a group of developers who need to work together (which these days, is nearly all projects, even if it’s merely asking another developer for help on your pet project). They remove some of the mental overhead of deciding how things should look, so that you can focus on the ideas instead.
For Angular, the style guide to beat is John Papa’s Angular Style Guide. It covers everything from how to name files, to how controllers/services/directives/etc. should be written, and where to declare your variables.
The Angular Style Guide has been written and updated to reflect the latest in opinions on the “correct” way to write and architect an AngularJS application. Part of the purpose of the style guide is to help ensure that code written today will be ready for Angular 2 when you decide to upgrade your application.
But what about existing code?
How Do I Refactor An Existing Angular Application?
Like many other developers, when I first started learning Angular, I followed the Phone Cat tutorial on the AngularJS website. If you’re not familiar, the tutorial takes you through the process of writing an application for viewing a catalog of cell phones and their details.
When I first did the tutorial, it had me put all of my controllers in one big controllers.js
file, all of the services in the services.js
file, etc. All of these practices are out of favor now.
In this blog post, I’ll be updating the tutorial to use the recommended practices of the Angular Style Guide. It’s likely I won’t follow every suggestion but I’ll try to follow most of them to bring this project up to date.
In a previous project, I took that tutorial and updated it to be a full MEAN stack application. Data was stored in a MongoDB instance and was served up by a NodeJS/Express server. To simplify this post, I switched the Angular service back to use $http
to grab the JSON file directly. In a follow up post, I’ll update the server for this application.
What Steps Do I Follow To Update An Angular Application?
The following steps are presented roughly in order. Each step will roughly correspond to a rule in the John Papa Angular Style Guide. You should be able to complete each step and reload the page to test your work.
- The old repo had Angular as a local library, so one of the first steps was to use NPM to install the latest version of all of the libraries and delete the local libraries
- Move the
'use strict';
to the top of the file - Replace
ModuleName.controller
withangular.module('ModuleName')
- This removes the module from the global namespace to prevent naming conflicts
- Move the anonymous controller function to a named controller function
- example:
.controller('NamedController', NamedController); function NamedController() {....}
- example:
- Use
$inject
to inject dependencies - Wrap all of the code in an IIFE
- Move all
var
declarations to the top of the controller with variables in alphabetical order- This only applies to variables that are “global” to the whole controller. Not variables that are local to a single function
- The idea is to provide a section you can glance at to see all of the variables quickly
- If the value assigned to the variable is a large object, try to create the variable with no value and assign the object lower down
- If the value assigned to the variable is a function, create a named function at the bottom of the controller and assign the name of the function to the variable
- Do not assign anonymous functions to a variable
- Mask
this
with thevm
(view model) variable. In other words,var vm = this;
at the top of the controller (before all other variables). If you’ve usedself
in the past, it should be replaced withvm
- Use search and replace to find any existing usage of
self
- If you are using eslint, you will need to add the following rule at the top of the file to satisfy it
/* eslint consistent-this: [2, "vm"] */
- Use search and replace to find any existing usage of
- Move all
$scope
bound variables to the top of the controller; just below thevar
(see controllerAs section below)- The idea is to provide a section you can glance at to see all of the variables quickly
- If the value assigned to the variable is a large object, try to create the variable with no value and assign the object lower down
- If the value assigned to the variable is a function, create a named function at the bottom of the controller and assign the name of the function to the variable
- Do not assign anonymous functions to a variable
- If you need an
init
function, the Style Guide recommends renaming it toactivate
- Switch to using
controllerAs
syntax. This is somewhat involved which is why I left it for the end- Change your
ng-controller
to use the controllerAs syntax. This will either be where the view is added to another view or in the routes section of app.js - Change your HTML template to use the new variable name. For example, if you used
LabelsController as Labels
, you would change the HTML to use something like {{Labels.currentLabel}}- Be sure to do the same anywhere you see ng-if, ng-show, ng-hide, ng-click, the array used by ng-repeat, ng-model, etc.
- In the controller, any variable assigned to
$scope
, should now be assigned tovm
- You can use search and replace but look out for
$watch
and$on
. Those will still use$scope
- You may need to update the variable referenced by
$watch
- You may need to update the variable referenced by
- At this point, you may be able to remove
$scope
from the list of injected services
- You can use search and replace but look out for
- Change your
- Rename files according to the file naming conventions,
component.component-type.js
and split files into component based files and directories
Be sure to perform the steps for all controllers, services, directives, etc. Almost all of them will apply to all types of files.
Code Examples
You can compare the before repo to the after repo to see all of the differences but here’s a quick example.
This is the original controller file from the tutorial:
// controllers.js 'use strict'; /* Controllers */ var phonecatControllers = angular.module('phonecatControllers', []); phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) { $scope.phones = Phone.query(); $scope.orderProp = 'age'; }]); phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) { $scope.isEditing = false; $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; } $scope.save = function(phone) { ... }; }]);
I’ve now cleaned up the code and split it into two component-specific controller files:
// phone-detail.controller.js 'use strict'; (function() { angular.module('PhoneDetailCtrl', []) .controller('PhoneDetailCtrl', PhoneDetailCtrl); PhoneDetailCtrl.$inject = ['$routeParams', 'Phone', '$log']; function PhoneDetailCtrl($routeParams, Phone, $log) { var vm = this; vm.isEditing = false; vm.mainImageUrl = ''; vm.phone = {}; vm.save = savePhone; vm.setImage = function(imageUrl) { vm.mainImageUrl = imageUrl; }; Phone.getPhone($routeParams.phoneId).then(getPhoneSuccess, getPhoneFailure); function getPhoneFailure(err) { $log.error("err", err); } function getPhoneSuccess(phone) { $log.log(phone.data); vm.phone = phone.data; vm.setImage(vm.phone.images[0]); } function savePhone(phone) { ... } } })();
// phone-list.controller.js 'use strict'; (function() { angular.module('PhoneListCtrl', []) .controller('PhoneListCtrl', PhoneListCtrl); PhoneListCtrl.$inject = ['Phone', '$log']; function PhoneListCtrl(Phone, $log) { var vm = this; vm.phones = []; vm.orderProp = 'age'; Phone.getPhones().then(success, failure); function failure(err) { $log.error("err", err); } function success(phones) { $log.log(phones.data) vm.phones = phones.data; } } })();
In the following blog posts, I’ll update the server file (see Part 2), change this application to use ES6 classes or TypeScript, and finally, upgrade it to an Angular 2 application.