$emit-ing Events from Outside AngularJS

I was working with on a little JavaScript project, and after being sufficiently annoyed and having to manage the DOM directly, decided to drop AngularJS in there. Oh. My. God. So much better. But I didn’t want to restructure everything, only where there was pain.

Pre-angular, there was some code like this in the app:

function draw() {
  // _many_ lines of nasty third-party blech
  updateStats(); // added by me
}
function updateStats() {
  // do some math
  // render some DOM
}

I definitely didn’t want to reimplement the whole draw function, but I did want to get that updateStats function into an angular controller, so I could use its goodness to actually do the DOM stuff. This is where I wanted to go:

function draw() {
  // _many_ lines of nasty third-party blech
  $scope.$emit('draw'); // this super doesn't work
}
// angular setup ...
app.controller('Ctrlr', function Ctrlr($scope) {
  $scope.on('draw', function updateStats() {
    // do some math
    $scope.stats = ...;
  });
  // other stuff ...
}

The question was “what actually needs to be there on line #3?” After a bit of digging around, I found it was pretty easy to make it work. I made a top-level $emit function to encapsulate the behaviour:

function $emit() {
  var scope = angular.element('body').scope();
  scope.$emit.apply(scope, arguments);
}
function draw() {
  // _many_ lines of nasty third-party blech
  $emit('draw'); // this works!
}
// angular setup ...
app.controller('Ctrlr', function Ctrlr($scope) {
  $scope.on('draw', function updateStats() {
    // do some math
    $scope.stats = ...;
  });
  // other stuff ...
}

…. except that my event handler doesn’t execute w/in a digest loop, so while the scope is updated, the DOM doesn’t repaint until something else applies it. Easy enough to fix with a little tweak to $emit:

function $emit() {
  var args = arguments;
  var scope = angular.element('body').scope();
  scope.$apply(function () {
    scope.$emit.apply(scope, args);
  });
}

Perfect!

I oversimplified a bit, however. The draw function is not just called from extra-angular code, some of that “other stuff” in the controller does too. You can probably see the problem: the controller functions are already w/in a digest loop, so $apply can’t be invoked again. This one, unfortunately required poking my nose into the angular internals (the stuff prefixed with $$), but for this project it is a reasonable choice to help bridge the conversion window. Angular keeps track of which phase it is currently in, and that’s exactly the piece of info we need. Here is the final version:

function $emit(name, eventArgs) {
  var args = Array.prototype.slice.call(arguments, 0);
  var scope = angular.element('body').scope();
  var phase = scope.$parent.$$phase;
  if (phase == '$apply' || phase == '$digest') {
    scope.$emit.apply(scope, args)
  } else {
    scope.$apply(function () {
      scope.$emit.apply(scope, args);
    });
  }
}

Seamlessly emitting events into my angular app from non-angular code, properly bound to the digest cycle, and allowing for re-entrant invocations/dispatch has already made the migration much simpler. I’d held off because I thought it was going to be complicated, but with 10 fairly simple lines of JS getting it done, I was a fool to wait.