152

I'm just starting with AngularJS, and am working on converting a few old jQuery plugins to Angular directives. I'd like to define a set of default options for my (element) directive, which can be overridden by specifying the option value in an attribute.

I've had a look around for the way others have done this, and in the angular-ui library the ui.bootstrap.pagination seems to do something similar.

First all default options are defined in a constant object:

.constant('paginationConfig', {
  itemsPerPage: 10,
  boundaryLinks: false,
  ...
})

Then a getAttributeValue utility function is attached to the directive controller:

this.getAttributeValue = function(attribute, defaultValue, interpolate) {
    return (angular.isDefined(attribute) ?
            (interpolate ? $interpolate(attribute)($scope.$parent) :
                           $scope.$parent.$eval(attribute)) : defaultValue);
};

Finally, this is used in the linking function to read in attributes as

.directive('pagination', ['$parse', 'paginationConfig', function($parse, config) {
    ...
    controller: 'PaginationController',
    link: function(scope, element, attrs, paginationCtrl) {
        var boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks,  config.boundaryLinks);
        var firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true);
        ...
    }
});

This seems like a rather complicated setup for something as standard as wanting to replace a set of default values. Are there any other ways to do this that are common? Or is it normal to always define a utility function such as getAttributeValue and parse options in this way? I'm interested to find out what different strategies people have for this common task.

Also, as a bonus, I'm not clear why the interpolate parameter is required.

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
Ken Chatfield
  • 3,277
  • 3
  • 22
  • 27

3 Answers3

271

Use the =? flag for the property in the scope block of the directive.

angular.module('myApp',[])
  .directive('myDirective', function(){
    return {
      template: 'hello {{name}}',
      scope: {
        // use the =? to denote the property as optional
        name: '=?'
      },
      controller: function($scope){
        // check if it was defined.  If not - set a default
        $scope.name = angular.isDefined($scope.name) ? $scope.name : 'default name';
      }
    }
  });
gae123
  • 8,589
  • 3
  • 35
  • 40
Hunter
  • 2,736
  • 2
  • 11
  • 3
  • 5
    `=?` is available since 1.1.x – Michael Radionov Feb 13 '14 at 07:10
  • 35
    If your attribute could accept `true` or `false` as values, you would (I think) want to use e.g. `$scope.hasName = angular.isDefined($scope.hasName) ? $scope.hasName : false;` instead. – Paul D. Waite Feb 27 '14 at 17:13
  • Wow, this is a really clean solution. Couldn't find such a good example for default value usage on the Angular Docs. +1! – Justus Romijn Jul 15 '14 at 08:55
  • 23
    Note: it only works with two-way binding, e.g. `=?`, but not with one-way binding, `@?`. – Justus Romijn Jul 15 '14 at 10:59
  • Is there a better way than doing `scope: { someValueAttr: '@someValue' }`, and in link function `scope.someValue = someValueAttr || 0`? Since you cannot write to a `@` scope variable. – Fredrik Boström Aug 29 '14 at 14:37
  • 20
    also can be done in template only: template: 'hello {{name || \'default name\'}}' – Vildan Oct 07 '14 at 01:28
  • 5
    Should the default value be set in the controller or in the `link` function? Based on my understanding, assigning during the `link` should avoid a `$scope.$apply()` cycle, shouldn't it? – Augustin Riedinger Mar 30 '15 at 14:38
  • 1
    `&?` defaults to `undefined` only starting with version 1.4.x. Before that you can use: `$attrs.optionalProp === undefined` – Eugene Kulabuhov Jul 06 '15 at 16:07
  • 1
    But this will only work assuming that `name` doesn't change after the first initialization, if `name` is changed to `undefined` later, from outside the directive, the code to assign the default will not be triggered. – QOI Apr 06 '17 at 09:42
  • Better answer here as it adhere's to the Angular API, this should be the accepted answer. – Kevinleary.net Jul 11 '17 at 16:29
  • Some notes: Without `?` it is still optional, there is just a subtle difference, defined in https://docs.angularjs.org/api/ng/service/$compile#-scope- (always hard to find back...). Somehow, $scope.optional === undefined is true in both cases. About later change mentioned by @QOI, target has to be notified, can be done by watching the attribute or the scope value. `?` now work on all kinds of binding. – PhiLho Jul 25 '18 at 08:35
111

You can use compile function - read attributes if they are not set - fill them with default values.

.directive('pagination', ['$parse', 'paginationConfig', function($parse, config) {
    ...
    controller: 'PaginationController',
    compile: function(element, attrs){
       if (!attrs.attrOne) { attrs.attrOne = 'default value'; }
       if (!attrs.attrTwo) { attrs.attrTwo = 42; }
    },
        ...
  }
});
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
OZ_
  • 12,492
  • 7
  • 50
  • 68
  • 1
    Thanks! So any thoughts on why `ui.bootstrap.pagination` does things in a more complicated way? Was thinking that if using the compile function any attribute changes made later would not be reflected, but this doesn't appear to be true as only the defaults are set at this stage. Guess there must be some tradeoff being made here. – Ken Chatfield Sep 13 '13 at 11:01
  • 3
    @KenChatfield in `compile` you can't read attributes, which should be interpolated to get value (which contains expression). But if you want to check only if attribute is empty - it will work without any tradeoffs for you (before interpolation attribute will contain string with expression). – OZ_ Sep 13 '13 at 11:12
  • 1
    Fantastic! Thanks very much for your clear explanation. For future readers, although tangential to the original question, for an explanation of what the 'interpolate' parameter does in the `ui.bootstrap.pagination` example I found this very useful example: http://jsfiddle.net/EGfgH/ – Ken Chatfield Sep 13 '13 at 11:22
  • Thanks a lot for that solution. Note that if you need the `link` option, you can still return a function in your `compile` option. [doc here](https://docs.angularjs.org/api/ng/service/$compile#-compile-) – mneute Jan 16 '15 at 14:47
  • 4
    Remember, that attributes needs the values as they would be passed from the template. If you're passing an array f.e. it should be `attributes.foo = '["one", "two", "three"]'` instead of `attributes.foo = ["one", "two", "three"]` – Dominik Ehrenberg Apr 22 '15 at 07:39
2

I'm using AngularJS v1.5.10 and found the preLink compile function to work rather well for setting default attribute values.

Just a reminder:

  • attrs holds the raw DOM attribute values which are always either undefined or strings.
  • scope holds (among other things) the DOM attribute values parsed according to the provided isolate scope specification (= / < / @ / etc.).

Abridged snippet:

.directive('myCustomToggle', function () {
  return {
    restrict: 'E',
    replace: true,
    require: 'ngModel',
    transclude: true,
    scope: {
      ngModel: '=',
      ngModelOptions: '<?',
      ngTrueValue: '<?',
      ngFalseValue: '<?',
    },
    link: {
      pre: function preLink(scope, element, attrs, ctrl) {
        // defaults for optional attributes
        scope.ngTrueValue = attrs.ngTrueValue !== undefined
          ? scope.ngTrueValue
          : true;
        scope.ngFalseValue = attrs.ngFalseValue !== undefined
          ? scope.ngFalseValue
          : false;
        scope.ngModelOptions = attrs.ngModelOptions !== undefined
          ? scope.ngModelOptions
          : {};
      },
      post: function postLink(scope, element, attrs, ctrl) {
        ...
        function updateModel(disable) {
          // flip model value
          var newValue = disable
            ? scope.ngFalseValue
            : scope.ngTrueValue;
          // assign it to the view
          ctrl.$setViewValue(newValue);
          ctrl.$render();
        }
        ...
    },
    template: ...
  }
});
Keego
  • 3,977
  • 1
  • 16
  • 9