20 November 2014

Nesting angular directives

Using angular you quickly find directives really handy. Often you can google a directive that does exactly what you need... almost. For example there is plenty of ready-to-use multichoice dropdowns. But let's say we need a dropdown that is automatically disabled when there is no possible choices. One way is to get the source and change it - with angular it's usually easy. But then we are stuck with this version and won't get any updates nor bugfixes. Better solution is to wrap an existing directive in our own.

Let's say we have an existing dropdown-choices directive that uses one parameter (dropdown-choices) for providing all possible choices and another one (dropdown-selected) for currently selected value. Let's use it to get what we need.

Tests first. Make sure your karma.conf.js contains a reference to dropdown-choices, for example:
module.exports = function(config) {
  config.set({
    ...
    files: [
      ...
      'app/vendor/*.js',
      ...
Example tests
'use strict';

describe('autoOffDropdown', function() {

    var autoOffDropdownDiv, scope;

    beforeEach(module('myModule'));
    beforeEach(inject(function($rootScope, $compile) {
        scope = $rootScope.$new();
        multicomboDiv = $compile('<div id="someId" auto-off-dropdown  \
                                       choices="choices" selected="selected"></div>')(scope);
    }));

    it('should be disabled when there is nothing to select', function() {
        scope.choices = [];

        expectSingleVisibleChildToBe("input:disabled");
    });

    it('should show div with dropdown when anything can be selected', function() {
       scope.choices = [1];

       expectSingleVisibleChildToBe("div");
    });

    function expectSingleVisibleChildToBe(jquerySelector) {

       scope.$apply();
       // may differ depending on angular version
       var visibleChildren = multicomboDiv.children().filter(
                                    function() {return $(this).css('display') !== 'none';});

       expect(visibleChildren.length).toBe(1);
       expect(visibleChildren.is(jquerySelector)).toBe(true);
   }
});
And now our directive
'use strict';

angular.module("myModule").directive('autoOffDropdown',function(){

    return {
        restrict: 'A',
        replace: true,
        scope: {
            choices:  '=',
            selected: '='
        },
        template:
            '
\
\
\
\ <input data-ng-show="noChoicesAvailable()" type="text" disabled="disabled" \ placeholder="No choices available"/> \
', link: function(scope) { scope.noChoicesAvailable = function() { return _.isEmpty(scope.choices); }; } }; });

Tested with angular 1.0.8