This is me Bressain's Blog

Creating External Hooks In AngularJS Directives

I ran into an interesting problem a few weeks ago at work. I was working on a stand-alone widget/directive that had the following requirements:

Since the widget would usually be in an Angular app, making an Angular directive seemed like the thing to do. The directive would also be offered as a self-bootstrapping chunk of javascript for pages that weren't using Angular but that's beyond the scope of this post.

Making the widget itself wasn't too bad but making those external hooks was a bear (for me anyway). The code is a little stripped down for public consumption but here's basically what I started with:

'use strict';

angular.module('comment-widget', []);
angular.module('comment-widget').factory('commentService', ['$http', '$q', '$window', function ($http, $q, $window) {
return {
getInitData: function (objectId, objectTypeId) {
var deferred = $q.defer();

$http.get('initdata?objectid=' + objectId + '&objecttypeid=' + objectTypeId)
.success(function (data) {
deferred.resolve(data);
})
.error(function () { deferred.reject({ commentCount: 0, resourceStrings: { commentButton: 'Comment' } }); });

return deferred.promise;
},

getCommentCounts: function (objectId, objectTypeId) {
var deferred = $q.defer();
var payload = { "objects": [{ "objectId": objectId, "objectTypeId": objectTypeId }] };

$http.post('counts', payload)
.success(function (data) {
deferred.resolve(data.counts.length > 0 ? data.counts[0].count : 0);
})
.error(function () { deferred.reject(0); });

return deferred.promise;
}
};
}]);
angular.module('comment-widget').controller('CommentCtrl', ['commentService', '$scope', function (commentService, $scope) {
initialize();

$scope.refreshComments = function () {
commentService.getCommentCounts($scope.objectId, $scope.objectTypeId)
.then(function (result) { $scope.commentCount = result; });
};

function initialize() {
commentService.getInitData($scope.objectId, $scope.objectTypeId)
.then(function (result) {
$scope.commentCount = result.commentCount;
$scope.resourceStrings = result.resourceStrings;
});
}
}]);
angular.module('comment-widget').directive('commentWidget', function () {
var template =
'<div>' +
'<button type="button" class="buttonL"></button><button type="button" class="buttonR"></button>' +
'';

return {
restrict: 'E',
replace: true,
scope: {
objectId: '=',
objectTypeId: '='
},
controller: 'commentCtrl',
template: template
}
});

First up was moving focus to another widget. This actually wasn't too bad but my first attempt was pretty terrible (it involved some external javascript object that was difficult to use). Thanks to some tips from a coworker, it was decided to use a bit of the jqLite included to trigger a jQuery event on button click.

link: function (scope, element, attrs) {
element.find('.buttonL').on('click', function () {
element.triggerHandler($.Event('comment.click', { objectId: scope.objectId, objectTypeId: scope.objectTypeId }));
});
}

And then the accompanying html:

<script>
$(function () {
$('#my-widget').on('comment.click', function (data) {
alert('comments was clicked with\nobjectId: ' + data.objectId + '\nobjectTypeId: ' + data.objectTypeId);
});
});
</script>

There are a few things that may seem odd here but they are super important to remember:

Like the previous requirement, my first attempt was pretty terrible until I brought jQuery events into the mix.

link: function (scope, element, attrs) {
element.on('comment.refresh', function () {
scope.refreshComments();
});
}
<script>
$(function () {
$('#refresh-button').click(function() {
$('#my-widget').trigger('comment.refresh');
alert('comments were refreshed');
});
})
</script>

I love the simplicity of the solution here. Through jQuery the external script sends an event and it's picked right up by the directive. Very, very slick.

What I learned through all of this was while I was beating my head against my keyboard trying to figure out how to come up with a solution that was "The Angular Way", I forgot that at the end of the day, it's just javascript. And because it's just javascript, you can usually pull in whatever library to help you accomplish your goals or even just do something in vanilla.js if it comes down to it.

If you're interested in the end result, head over to this gist here. Getting that directive under test was half the battle as well so all of the tests are included. Please leave a comment if you have a question or if something is missing.