Almost @Decorate

This is a handy little snippet I’ve been using to emulate ES7′s decorators without needing to depend on a non-standard syntax, nor wire in extra Babel (or whatever) plugins. Or even use more than ES3, once the function itself is transpiled.

const decorate = (...decorators) => component =>
    decorators.reduce((c, d) => d(c), component);

Consider this ES7 decorated component (using the syntax as of 2017-08-25):

@MyDecorator(/* my config */)
@OtherDecorator(/* other config */)
class Fred {}
export default Fred;

Without decorators, it looks like this:

class Fred {}
export default OtherDecorator(/* other config */)(
    MyDecorator(/* my config */)(
        Fred
    )
);

I find this hard to read, even before complicated configuration (functions returning objects of functions, anyone?). Even if you play around with different line breaks, indentation, and intermediate variables, it’s never super clean. Also, the nesting is in the “reverse” order of how decorators apply. You often see a temp variable to fix this latter issue:

class Fred {}
const wrappedFred = MyDecorator(/* my config */)(
    Fred
);
export default OtherDecorator(/* other config */)(
    wrappedFred
);

Still a lot of cruft. Now if we use that decorate function up top, we get this:

class Fred {}
export default decorate(
    MyDecorator(/* my config */),
    OtherDecorator(/* other config */)
)(Fred);

Not as nice as today’s candidate ES7 syntax, but only barely. And it’s also completely transparent, unlike the Babel plugin (found at https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy/blob/v1.3.4/src/index.js for the masochists among us).

Update: if you don’t have an ES6 transpiler and need ES3, here it is:

function decorate() {
    var l = arguments.length, decorators = Array(l);
    for (var i = 0; i < l; i++) {
        decorators[i] = arguments[i];
    }
    return function(component) {
        return decorators.reduce(function(c, d) {
            return d(c);
        }, component);
    };
};

$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.