You've been given an Angular code base. How do you figure out where everything is? Where scope variables are coming from? What controller does what?
Angular doesn't make it easy, but there are few tips and tricks that can make your life easier. Tools like Angular Batarang can help, but they're not always installed when you need them. Take some time to understand the underlying concepts, and your life will be easier in the long run.
Angular Batarang is not a great way to do this kind of work. I would not recommend trying it out until you've attempted these techniques...
Here are a few tips that will save you a couple of hours next time you are thrown in the deep end of an Angular code base.
Most of these tips require that debugging info be available. If they don't work for you, open up the console and type
angular.reloadWithDebugInfo();
What is the scope for this element?
Almost everything in Angular revolves around the scope. Angular creates a scope hierarchy with controllers and directives . Directives can sometimes have their own isolate scope. Being able to know the value of the scope at a given place in our page is valuable to debugging, and easy to do. Angular lets you access the scope available to an element via the .scope
method.
An isolate scope is one that is unique to the directive and is not affected by anything outside of it (aside from bindings)
- In Chrome, right click an element in the page you're interested
- Select inspect element
- The element is now highlighted in the inspector
- In the console, enter
$($0)
to get the element. If not using jQuery, you can useangular.element($0)
$($0).scope()
will return the scope associated with the element. You can see its properties right away.- Properties inherited from parent scopes won't show up, but you can still type their name. So even if you don't see a property
foo
on your scope, you can run$($0).scope().foo
and see its value if it is available from a parent. - You can look at the properties of the parent scope with
$($0).scope().$parent
. You can chain this too:$($0).scope().$parent.$parent
- You can look at the root scope:
$($0).scope().$root
- If you highlighted a directive with isolate scope, you can look at it with
$($0).isolateScope()
- If you highlighted an element inside of a directive with isolate scope,
$($0).scope()
will show what is available to it (ie: the isolate scope)
It can be a bit confusing, but try it out! It can be a lot faster than setting breakpoints and inspecting variables. If you're not sure which of the above commands to run, just try them all. They're perfectly safe, and you'll eventually find the data you're looking for.
For more information on the angular scope, refer to the angular documentation
Where does this variable come from?
Once your Angular application grows in size it can become challenging to figure out where variables used in our templates come from. For a single directive with isolate scope, it's easy. In a hierarchy of controllers and directives, try some of these methods for tracking them down:
- Using the method in the previous section, find the element using the variable and look at its scope.
- Try to inspect the value:
$($0).scope().foo
if the property is called foo. - If the value is defined, you have your answer, the property came from the controller or directive at that level.
- Otherwise, go up one level in the DOM, and try again.
- Repeat until you find it.
There is one small details that make things more complicated however: scope inheritance. A controller up in the hierarchy could be providing values available on the child scopes. There's a few ways you can go about figuring it out:
You may have noticed elements with a class "ng-scope" when looking through the inspector. They represent the elements where new scopes got created
- You can use
$($0).scope()
directly: the inspector will only show the direct scope's properties, not the ones defined on parents. So you can go up the hierarchy until you see your property listed. - Use
$($0).scope().hasOwnProperty('foo')
to see if the property is set directly on the scope you're looking at, and go up the hierarchy until it returns true. - Keep going up until you no longer see your property. Just add .parent() to the element to do it from the console:
$($0).parent().scope().hasOwnProperty('foo')
- Look on the last element where you saw it: your property was defined in a controller or directive defined on that element, or a close parent. It could also be defined in the markup (like in the case of an ng-repeat). You can do this by outputting the element directly in the console.
Using the above techniques, you'll be able to find at what point in the document the property is being set. It's definitely a lot of work, but is usually faster than trying to guess, unless you wrote the code yourself!
Why does Prototypal Inheritance matter here?
Inheritance in JavaScript is different from inheritance in languages like Java or C#. JavaScript uses prototypal inheritance. Accessing a value that doesn't exist on a given object gets delegated to one higher up in the chain, if available.
Prototypal inheritance is amazingly useful when debugging Angular applications (see link below). The techniques above use it to analyze scopes to find where values come from. If you've ever had issues getting your two-way data bindings to behave the way you expect, the culprit may be prototypal inheritance.
There are many tutorials on how inheritance work in JavaScript on the net. You may want to start here. A good understanding of these concepts will make your Angular experience a lot smoother. It's a good foundation for any JavaScript developer.
What controller handles this section of the page?
This may seem obvious, but we often forget about it. If you need to figure out which controller is responsible for a section of the page, just look for the ng-controller
attribute in the DOM. If you don't see one, go up one level and look again.
If you're like me, you may have trouble finding an attribute in a ton of HTML. To find find it, click on an element, run $($0).attr('ng-controller')
, and if nothing comes up, click on its parent (using the bread crumbs).epeat until you get a controller name back.
With a big of jQuery magic, we can also find the controller that is nearest to our selection in one step: $($0).closest('[ng-controller]').attr('ng-controller')
Why don't some elements show up?
If you use ng-if
, ng-switch
, or ng-views
, you may have wondered why your elements didn't show up when searching for them in the inspector. Unlike ng-show
which shows and hide elements by changing their visibility, these directives add and remove their content from the DOM completely based on conditions or routes.
Angular leaves a comment, such as <!-- ngIf: isFoo -->
where the element would have been if isFoo
had been true.
If the condition is more complicated, you can use the techniques we described earlier to understand why an element isn't being shown. In For this example, you would run $($0).scope().isFoo
, and you would get a falsy value. That's why the element didn't get added to the DOM, and was replaced by a comment instead.
The comment will be in the DOM when the element has been added, not just when it is absent. Look right below it for an element with the same condition. If there isn't one, then you can assume the condition was not met.
How do I see the effects of a value changing?
What if we want to force the element from the previous section to show without going through the scenario that would set isFoo
to true
? We can do it from the console!
- Select the element in the inspector.
- run
$($0).scope().isFoo = true
to setisFoo
- run
$($0).scope().$digest()
to force Angular to reevaluate the value - The DOM should change to reflect the new value
The first step, selecting the element, is very important. If you don't select the right element, you may or may not get the results you expect because of how prototypal inheritance delegation work. Your value may not always get propagated up or down the chain.
What is the digest cycle?
In the previous section, we used the $digest method to trigger changes. You may wonder why we had to do that. Why didn't the DOM change the moment we set isFoo
to true?
Objects can't notify Angular that their properties have changed without proxies or custom setters. One Angular's goals is to let us use plain old JavaScript objects. Angular solves this using the digest cycle to inspect expressions and watchers for changes. Digest is run by Angular when an event it is aware of happens. This could be a an $http
request or an ng-click event.
Proxies are a new feature of ES2015 http://babeljs.io/docs/learn-es2015/#proxies
When something happens outside of Angular's known world about (maybe a jquery plugin, or a console command), it doesn't automatically run $digest
and nothing happens. In this case we'll need to execute $digest
ourselves. As soon as the digest executes, things will get updated.
ES2015 is the next version of JavaScript, also known as ES6. It contains features that can be used by Angular in the future to improve performance by simplifying how object changes are detected, such as proxies. While it is possible to use most of ES2015's features using Babel, a javascript compiler, fast proxies must be implemented natively, so the current version of Angular uses dirty checking instead.
How does Angular DI work?
Angular is built on top of the concept of dependency injection. It can feel a little bit magical. While a full explanation of Angular's DI is out of the scope of this blog post, here's a few things to keep in mind as you wrap your head around it.
Note that the following is a big oversimplification!
- There's no magic behind Angular's DI. Under the hood, Angular keeps a cache/map of keys to objects. When a key is in your component's dependency (such as $scope, $http, or one of your own services), Angular looks it up in the cache and passes the value to your method when creating your component.
- Angular services, factories and providers, values and constants are almost all the same thing. They just differ in what kind of control they give you when they are created, and if they can be used in config blocks or not.
- Under the hood, directives append "Directive" to their name. That is why they don't conflict with other services of the same name.
- A provider will have the suffix "Provider" when injected in a config block, but won't when injected elsewhere (where the result of its
$get
method is injected instead) - All of those objects are singletons, so you're using the same instance everywhere (within one page load). That is even true of factories and providers. The function they provide gets called once per page load and the result is reused everywhere it is injected.
I strongly recommend learning the different types of angular services. They are a significant source of confusion when trying to wrap your head around an AngularJS code base.
Breakpoints are your friends.
You don't have to debug through your JavaScript code by adding console.log
or debugger
statements everywhere, though it can be useful. Consider familiarizing yourself with breakpoints, available in the inspector of all major browsers.
Chrome even supports conditional breakpoints, where the breakpoint will only stop execution if a certain JavaScript expression returns true. This is useful when debugging repetitive code where only a specific iteration is interesting (such as nested loops). You don't have to go through the entire loop to get where you want.
It's all JavaScript.
At the end of the day, the most important thing to understand about AngularJS, is that It's just JavaScript. There's no magic, and it is bound by the same rules as any other JavaScript applications. Dependency injection is just a big list of key value pairs from which objects are pulled from and passed to your components. The way scopes work is based on prototypal inheritance. The $digest
loop is just a task that iterates over expressions and watchers when Angular-related events occur. You can force it to happen.
By understanding the good old JavaScript under Angular's hood, you can reduce the time it takes to debug your code base. It is worth the time to take a look at how Angular implements its own methods, including the directives for basic form elements from the Angular repository. It takes a bit of time, but gives a lot of background on how things end up working.
CC background photo by flickr user Jon Candy, "Longleat Maze"