Skip to content
← All posts
6 min read Dawid Skłodowski

Getting started with AngularJS: directives and two-way binding

AngularJS makes the DOM react to your data automatically. A practical introduction to two-way binding, directives, controllers, and dependency injection — plus an honest look at the digest cycle and where the magic costs you.

For years, front-end JavaScript meant jQuery: select an element, read or write its contents, bind an event, repeat. It works, but on anything with real interactivity it becomes a tangle of manual DOM updates where the page and your data are always one missed line away from disagreeing. AngularJS proposes something fundamentally different — describe the relationship between your data and the DOM once, and let the framework keep them in sync. After a few projects with it, here is the practical introduction we wish we had started with, magic and costs included.

Two-way binding: the headline trick

The feature that makes people sit up is two-way data binding. You declare a binding in the markup, and from then on the model and the view stay synchronised automatically — in both directions, with no glue code:

<div ng-app ng-controller="GreetController">
  <input ng-model="name" placeholder="your name">
  <p>Hello, {{ name }}!</p>
</div>

Type in the input and the paragraph updates as you type. Change name in JavaScript and the input updates. There is no $('#name').on('keyup', ...), no .text(...) call — the ng-model and the {{ name }} interpolation declare that these two things are the same value, and Angular does the rest. For forms, filters, live previews, and dashboards, this removes an enormous amount of the busywork that jQuery code is made of.

Controllers and scope

The data behind a chunk of UI lives on a $scope, wired up by a controller:

angular.module("app", [])
  .controller("TodoController", function ($scope) {
    $scope.todos = [
      { title: "Write the blog post", done: false },
      { title: "Ship it",             done: false },
    ];

    $scope.remaining = function () {
      return $scope.todos.filter(function (t) { return !t.done; }).length;
    };

    $scope.add = function (title) {
      $scope.todos.push({ title: title, done: false });
    };
  });
<div ng-controller="TodoController">
  <p>{{ remaining() }} left</p>
  <ul>
    <li ng-repeat="todo in todos">
      <input type="checkbox" ng-model="todo.done">
      <span>{{ todo.title }}</span>
    </li>
  </ul>
  <input ng-model="newTitle">
  <button ng-click="add(newTitle)">Add</button>
</div>

$scope is the bridge between controller and view: properties and functions on it are directly addressable in the template. ng-repeat renders the list and keeps it in sync as todos changes; tick a checkbox and remaining() updates instantly, because the checkbox is bound to todo.done and the view re-evaluates. Notice there is no DOM manipulation anywhere — you describe what the UI is as a function of the data, not the steps to change it.

Directives: teaching HTML new tricks

Directives are Angular’s most powerful and most distinctive idea. ng-model, ng-repeat, and ng-click are all directives — markers on DOM elements that attach behaviour. The real power is that you can write your own, packaging a piece of UI into a reusable custom element or attribute:

angular.module("app").directive("starRating", function () {
  return {
    restrict: "E",                       // use as an element: <star-rating>
    scope: { rating: "=" },              // two-way bind a 'rating' attribute
    template:
      '<span ng-repeat="n in [1,2,3,4,5]" ng-click="rating = n">' +
      '  {{ n <= rating ? "★" : "☆" }}' +
      '</span>',
  };
});
<star-rating rating="review.score"></star-rating>

Now <star-rating> is a component you can drop anywhere, with its own template and its rating two-way bound to whatever you pass in. This is the same instinct that will later drive web components and every component framework that follows: encapsulate markup, styles, and behaviour into a named, reusable unit. Angular got there early, and directives are where the framework’s ambition really shows.

Dependency injection and services

Angular has dependency injection baked in, which is unusual for front-end JavaScript and genuinely useful. You ask for what you need by name in a function’s arguments, and Angular provides it:

angular.module("app")
  .factory("TodoApi", function ($http) {
    return {
      all:    function () { return $http.get("/todos"); },
      create: function (t) { return $http.post("/todos", t); },
    };
  })
  .controller("TodoController", function ($scope, TodoApi) {
    TodoApi.all().then(function (res) { $scope.todos = res.data; });
  });

The controller declares it needs $http (Angular’s HTTP client) and your own TodoApi service, and gets them injected. Pushing server communication into a service keeps controllers thin and makes the data layer testable in isolation — the same separation-of-concerns discipline we value on the Rails side, applied to the browser. For a Rails app, TodoApi talks to your JSON endpoints and Angular renders the result, a clean split between a server that serves data and a client that presents it.

The digest cycle: where the magic comes from, and costs

It is irresponsible to teach Angular without explaining how the binding actually works, because that is where its limits live. There is no real “magic” — there is a digest cycle. Angular keeps a list of watchers (one per binding) and, after any event it knows about (a click, an HTTP response, a timeout), it runs a loop that checks every watched value against its previous value and updates the DOM where they differ. It repeats until nothing changes — that is “dirty checking”.

This has two practical consequences you must internalise:

  • Performance scales with the number of bindings. Every binding is a watcher checked on every digest. A page with a few hundred is fine; a grid with thousands of live bindings will make the digest slow and the UI janky. The fixes are real techniques you will reach for — one-time bindings ({{ ::value }} in 1.3), track by in ng-repeat, and not binding what does not need to change.
  • Angular only knows about changes it caused. If you change data outside Angular’s world — a raw setTimeout, a third-party callback, a jQuery event — the digest does not run and the view goes stale. You have to tell Angular with $scope.$apply(...). The “why isn’t my view updating?” question almost always ends here.

Should you use it?

For a genuine single-page application — rich, stateful, lots of interactive UI over a JSON API — AngularJS is a big step up from hand-rolled jQuery. Two-way binding and directives remove a category of bugs and a lot of code, the structure (controllers, services, DI) scales to a real application, and the component thinking is the right direction for the whole industry. The costs are equally real: the digest cycle is a performance model you must respect, the framework is opinionated and large, and sprinkling a little Angular onto an otherwise server-rendered page is awkward — it wants to own its corner of the DOM.

Our rule of thumb in 2014: reach for Angular when a screen is truly application-like, and stay with server-rendered HTML (plus small, targeted JavaScript) when it is mostly content. Used for what it is built for, it changes how much you can comfortably build in the browser — just go in understanding the digest cycle, because that understanding is the difference between Angular feeling like magic and Angular feeling like a mystery.