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
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
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 use
$($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
fooon your scope, you can run
$($0).scope().fooand 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:
- You can look at the root scope:
- If you highlighted a directive with isolate scope, you can look at it with
- 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().fooif 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.
$($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:
- 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?
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.
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:
Why don't some elements show up?
If you use
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
true? We can do it from the console!
- Select the element in the inspector.
$($0).scope().isFoo = trueto set
$($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?
$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.
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
$getmethod 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.
debugger statements everywhere, though it can be useful. Consider familiarizing yourself with breakpoints, available in the inspector of all major browsers.
$digest loop is just a task that iterates over expressions and watchers when Angular-related events occur. You can force it to happen.
CC background photo by flickr user Jon Candy, "Longleat Maze"