AngularJS best practices: Refactoring existing code to Angular

Tags:

I’ve been involved in several projects where I worked on moving an existing codebase into using AngularJS. Some involved refactoring a small codebase, which is relatively straightforward in most cases, but I’ve also refactored much larger projects where moving everything at once is not an option.

Here’s some best practice type stuff that I’ve learned along the way.

Start small

Regardless of the type of code you’re refactoring, it’s usually best to start with a small component. It doesn’t always make sense to move everything into using some other library just because you can, but rather just change things as you need to develop new features or fix bugs.

The simplest way to start is by taking a part of the code and replacing it with an Angular controller. Take away any code that modifies the DOM, and instead build it into the markup using Angular’s databinding features.

You’ll definitely encounter cases where you need to access some more general code when doing this, which is a perfect case to move the code into a service. Then, it’s very easy to inject it into your controller.

Organizing things

It’s usually a good idea to keep things simple. When you start, just create a page-specific angular module. This is probably quite obvious if you’re working with a single page app, but in a case where your app has traditional page changes, keeping modules on their own pages for starters is a good idea.

As you keep adding things into the module, you’ll probably start seeing things that should be shared between controllers. In a case like that, you should move the code into a service.

Eventually you’ll start seeing cases where you need the same functionality in different modules. When this happens, create a base module and put the shared things there. You can then add a dependency to the base module into your page-specific modules.

Dealing with the DOM

You can easily move much of legacy DOM handling code into Angular simply by using Angular’s builtin directives and databinding features. However, in the likely case that you have used some jQuery plugins or done some other slightly more involved DOM trickery, things may be a tiny bit more difficult to sort out.

The most straightforward things can easily be wrapped into a directive. Sometimes the only thing you need to do is take the jQuery (or other) function call, wrap it in a directive and call it a day.

For anything nontrivial, it’s usually worth checking out whether you can simply plug in Angular UI and be done with it. Even if that’s not the case, you can use their code as an example on how to do more complex plugin integrations or such into directives.

The great thing is once you’ve done it once, you can easily reuse the directive anywhere. Be sure to open-source it if possible, as people will certainly appreciate your work!

Working with legacy code

Especially with large applications, it’s often not feasible to move everything into Angular at once.

You’re going to need to access your Angular services from some of the legacy code, and you’re also going to need to be able to respond to changes that happen via legacy event handlers inside your Angular code.

Accessing services

You can access your Angular services (and anything else as well) by using your application’s injector. There’s only one instance of the injector, and there doesn’t seem to be any convenient way of accessing it as of writing this, so what you need to do is the following:

angular.element(document.body).injector().get('nameOfService');

You should replace document.body in the above snippet with the element where you initialize your application (eg. where your ng-app is, or the element you used angular.bootstrap on)

Reacting to non-Angular events in Angular code

A typical case is also where you have some legacy event handlers or other code, which then should cause something to happen in your Angular-handled code. For example, maybe you have a button which needs to cause an item to appear in an Angular based list, or something along those lines.

The simplest way to deal with this is to use events. If you use jQuery or some other library with its own event support, you can use that, or you can also use events directly from Angular’s root scope.

With jQuery, you can use the following pattern:

//a jQuery handler that triggers an event
$('.some-button').click(function() {
    $.event.trigger('SomeEvent', { foo: 'bar' });
});
 
//And this code would be inside your angular code to handle the event:
$(document).on('SomeEvent', function(ev, data) {
    $scope.$apply(function() {
        //handle event
    });
});

Using the root scope object in Angular, you can do…

//some event handler
someElement.onclick = function() {
    angular.element(document.body).injector().get('$rootScope').$broadcast('SomeEvent', { foo: 'bar' });
});
 
//And this code would be inside your angular code to handle the event:
$scope.on('SomeEvent', function(ev, data) {
    //do something
});

Other things

The usual recommendations for refactoring code also apply, with good test coverage being one of the important ones. If you have good test coverage on the code, you’ll have much better chances of catching any bugs that may be accidentally introduced with the refactoring. If you don’t have any, it’s probably a good idea to look at the Angular developer’s guide testing chapters.

You might also need to get data from your backend into Angular. The most common way in Angular to do this is to just use Ajax, but especially in a case where you’re working with legacy code, you might already be outputting the data in some other way. I’ve written about that before, so check out the post for how to get backend data to Angular without Ajax.

In closing

Have you refactored legacy codebases to Angular? What gotchas did you run into? Did you find any other best practices? Do leave a comment if so, or if you have any questions.