Web apps are increasingly getting more and more complicated these days. Libraries and frameworks of the past just aren't built to grow with these evolving needs. Using jQuery to manage a complex web app just isn't going to cut it. Using Backbone will take you only so far before it falls flat on its face and you find yourself in a tangled mess. Fortunately, new libraries are emerging to meet these challenges head on, improve development over time, and allow us to continue to push forward with building better apps. AngularJS is one of those.
AngularJS is a JavaScript framework for building applications that run in a web browser. It's a fresh new take on how to approach building web apps that have complex interactions. One of its greatest strengths is its bi-directional data binding capabilities, which, take care of updating the UI when data changes. This data change could be caused by a user inputting some data into a form or from a backend service suppling new data to the application. Either way, AngularJS frees the developer form wiring all of the event bindings.
Before we dive in to bi-directional data binding specifics let's take a brief look at what AngularJS offers.
Working with AngularJS' Built-In Tools
AngularJS is complex enough that you can get lost figuring out what's important. Additionally, AngularJS is un-opinionated, i.e., it doesn't dictate what your objects look like or how to communicate with a server. While I personally like the way AngularJS handles these things, it makes the learning curve slightly steeper. To help counteract this sharp ascent, I've taken the liberty of compiling my thoughts gained from a year of learning and using AngularJS.
Dependency Injection
Using AngularJS' DI
(Dependency Injection) system is the best way to modularize your AngularJS code. Don't forget to use ngmin before you minify your JavaScript. Use a build tool such as Grunt, Gulp, Yeoman, or Lineman.
Factory
Use AngularJS factories to create your object instances. In your factory definition return your prototypes
(or in CoffeeScript, your Classes
). You can use your prototype (or Class) within the factory to wrap a resource returned from an API. Here you can insulate yourself from any oddities with the API's data model.
If the application is your hand and API's json is chocolate, then a factory is a hard candy wrapper that keeps things clean. We borrowed concepts from our favorite languages and created a base accessor factory to wrap all json coming in from the API. When we needed to expand we simply subclassed the base factory into specific factories for the object, adding functions for logical checks. Because we put this logic in the object itself, we could do things like asking a user object if it is allowed to do this or that i.e. user.may_use_feature()
or return computed attributes user.get('fullNameWithTitle')
.
Service
Use services when calling an API or storing application state. Services are singletons. Updates to them are propagated to each place they are referenced. When using a service to wrap an API, it's a great place to contain any differing assumptions about the API that do not fit with your application.
We used services to wrap all API calls allowing us to have a comfortable boundary between our application and API. By structuring the application this way, we had a place for all of the pre and post processing needed. It also allows organizing API abstractions. As one example, we had a rich internal representation of a user. Fully hydrating our User
required pulling information from multiple sources. We had our user service make multiple API calls to retrieve all the information, wrap it in our User factory and presented it to the rest of the application as one resource.
We had lots of success with this Service/Factory pattern while working with an ever-evolving API. As the API changed, we only had to refactor the small corresponding parts in the respective Service or Factory. See AngularJS Services and Factories done right for an example how to put it all together.
Controller
An AngularJS controller is where you handle interactions with the user. It also is where the bi-directional JavaScript to HTML data binding occurs via AngularJS' $scope
. Many controllers with limited responsibilities works better in the long run than fewer with larger roles. AngularJS allows multiple active controllers, and they can be arranged in a hierarchy.
Each controller instance gets it's own $scope
including controllers of the same type. AngularJS also alters JavaScript's this
in context of a controller. If you find yourself struggling with controller communication, you should delegate that particular part to a service. A child controller can read it's parents $scope
, but anything else becomes problematic.
For readability have an HTML view "own" a controller instead of being assigned one in the router. This allows better controller and view reuse between AngularJS applications, and speeds up development when trying to determine which controller is governing view behavior. It also encourages smaller controllers.
Directive
Directives are for extending HTML elements and attributes then associating behavior with it.
After bi-directional binding, directives seem to be the second most advertised feature of AngularJS. They are however; one of the dark corners of AngularJS as acknowledged by it's creators
When starting out, forgo writing directives. You need a deep knowledge of AngularJS' internals to debug directives when they misbehave. Instead, start by writing a small controller and view. You can refactor these into a directive later when it feels right.
jQuery
Take care with other libraries that modify the DOM, including jQuery. Anything DOM related within ng-app
is owned by AngularJS, and it expects it won't be altered outside of a digest
cycle. Mainly as a challenge for ourselves, we avoided using jQuery entirely in a large project full of complex interactions. We were very satisfied with the result. If you choose to include jQuery in your project be sure to read up on AngularJS' digest
cycle.
Structured, yet flexible
AngularJS provides enough scaffolding to build on top of, but not to too much as to obstruct developer creativity. Encouraging organization and modularization; Factories and Services are simply AngularJS DI constructs to assist the developer in organizing their code. What roles either of them play are entirely up to the developer.
The ultimate reason for using AngularJS is not for what it can do, but what it frees the developer from having to do. Free from wiring up complex bindings, developers can focus on creating a product instead of creating a system to create a product (related: essential complexity vs. accidental complexity) Picking the right tool for a job is part of software craftsmanship and the benefits of AngularJS coincide nicely when the goal is a web application.
Also read the followup blog post AngularJS Services and Factories done right