Last updated at Mon, 06 Nov 2017 18:49:47 GMT
When it comes to frameworks, no one is perfect. As we migrate the Logentries application from legacy code to Angular, we’ve encountered a few interesting challenges along the way that we’ve enjoyed investigating and resolving. While specific challenges often depend on your project and migration strategy, the aim of this post is to share our solutions to problems one may encounter when migrating an app to Angular. In particular, I’ll focus on how Angular handles routing and some issues we’ve encountered along the way.
Background
Before diving into the challenges we faced with routing, I want to provide a brief summary of what we did before the migration to Angular and why we chose to migrate. Before using Angular, most of the application was initially written with JQuery and relied on the BBQ plugin for routing. BBQ would essentially push query strings into the url, triggering screen changes and calling appropriate javascript code. While this seems simple enough in theory, we started running into some issues. There was no list of actual url’s that the application supported, nor was there a default route. Sometimes we had issues with BBQ pushing unwanted states into the url. We ultimately decided to move away from BBQ and introduce Angular routing using the $routeProvider
service.
Let’s look at how to create a simple route:
.when(‘/car/:car_id/', {
controller: 'CarController',
templateURL: ‘car.html’,
})
This route enables us to go to url /car/2
and define parameters via the colon syntax, which is then accessible in the controller via the $routeParams
service. What if you want to create a route which doesn’t have a template associated with it? Initially, the thought would be to remove the templateUrl
, but that would result in a error. If you want to declare a route with no template, use a template with empty string like this:
.when(‘/car/:car_id/', {
controller: 'CarController',
template: ‘ ’,
})
My route does not load
Sometimes route changes aren’t triggered. One common reason for this is if you click a link to a route or programmatically switch a route to the same one you are currently on. Let’s imagine you have a tree selector with different cars from your fleet. Selecting a single car brings you to a route /car/:car_id
, but selecting more than one car brings you to a route of /cars
. If you are currently in a /cars
route and want to select more cars (for the purpose of this example, selecting more than one car triggers a /cars
route change), you are changing the route to the one you are currently in. When this happens, Angular will sometimes detect that you are in the route you want to change to doesn’t trigger the route change. The solution to this problem is to add trailing slash to the url, so /cars
becomes /cars/
. This will trigger the route change even if the current route is the same.
My controllers get initialized twice
Not so long ago, we discovered an interesting “feature” of routing in Angular: some of the controllers were getting initialized twice whenever a route change happened. The cause of this issue wasn’t so obvious. Most related issues we researched were due to loading a controller with a route and declaring the ngcontroller
directive on a template (which basically means initializing the controller twice), but this wasn’t the case for us. We decided to debug the issue from its actual route. Angular comes with a few handy events that are fired on route changes. $locationChangeStart
gets broadcast before a url change happens (this is especially helpful if you want to prevent a route change) and $locationChangeSuccess
gets broadcast after a successful url change. The first event was the one that pointed us towards to answer to our problem. $locationChangeStart
was firing twice, prompting us to look at what was triggering the route change. Most of the route changes were handled by the $window
service. As it turns out, the $window
service is a wrapper for the window object and basically has no idea what is going on inside Angular. The solution was to change the $window
service to $location
. Angular’s documentation explains the difference between using $location
and $window
here: https://docs.angularjs.org/guide/$location.
From the $location
service, we used $location.url()
, which takes a url as a parameter and changes the route. This was only half the job done – it turns out that $location.url()
doesn’t always trigger a digest cycle. To make sure this happens consistently, we wrapped the call in a timeout, ensuring $location.url()
happens during the next digest cycle:
var url_to_change = ‘/cars’
$timeout(function(){
$location.url(url_to_change)
})
Since there was a lot of places where url changes were happening, we created a simple service which changes the route in the application:
angular.module('myApp')
.service('RoutingService', ['$timeout', '$location',
function($timeout, $location) {
"use strict";
var changeUrl = function(url){
$timeout(function(){
$location.url(url)
})
};
return {
changeUrl : changeUrl
}
}
]);
Few tips for Routing
Angular’s $routeProvider
comes with a really handy piece of functionality called Resolve
. It’s an optional map of dependancies that can be injected into the controller. If any of its dependancies are promises, Angular will wait for these promises to finish before the controller is instantiated. This means we can inject services to the Resolve
option that fetches data before we actually pass that data into our controller.
Query strings in in the url can also cause some headaches but can be addressed in a few different ways. We found success using the $location.search()
function. When called without any parameters, it returns an object with a key value pair representation of the query strings. We can also pass an object with key value pairs to the search()
function, causing a route change with updated query strings.
To conclude, Angular’s routing is pretty powerful and can make your application a lot easier to maintain. While the issues described above are specific to our project, the solutions are best practices worth considering for any Angular app.