31

I'd like to bind/set some boolean attributes to a directive. But I really don't know how to do this and to achieve the following behaviour.

Imagine that I want to set a flag to a structure, let's say that a list is collapsable or not. I have the following HTML code:

<list items="list.items" name="My list" collapsable="true"></list>

items are two-way binded, name is just an attribute

I'd like that collapsable attribute to be available in the list's $scope either by passing a value (true, false or whatever), either a two-way binding

<list items="list.items" name="{{list.name}}" collapsable="list.collapsed"></list>

I'm developing some UI components and I'd like to provide multiple way of interacting with them. Maybe, in time, some guys would like to know the state of that component, either is collapsed or not, by passing an object's property to the attribute.

Is there a way to achieve this? Please correct me if I missunderstood something or I'm wrong.

Thanks

Chris X
  • 901
  • 3
  • 9
  • 19
  • 1
    setting the attribute as two-way binded with `'='` won't work when passing direct boolean values `true` or `false` because I can't set the scope's value to another one. e.g. `$scope.collapsable = false;` – Chris X Aug 02 '13 at 10:18
  • will you collapsable attribute value change at run time or remain the same at the time of deslaration – Ajay Beniwal Aug 02 '13 at 10:25
  • if values is not going to change then just use attrs["collapsable"] – Ajay Beniwal Aug 02 '13 at 10:29
  • as I said, the value might change within the controller – Chris X Aug 02 '13 at 10:35

5 Answers5

26

You can configure your own 1-way databinding behavior for booleans like this:

link: function(scope, element, attrs) {

    attrs.$observe('collapsable', function() {

        scope.collapsable = scope.$eval(attrs.collabsable);
    });

}

Using $observe here means that your "watch" is only affected by the attribute changing and won't be affected if you were to directly change the $scope.collapsable inside of your directive.

bingles
  • 11,582
  • 10
  • 82
  • 93
  • 1
    I find using $scope/$element/$attrs names here confusing, because link does not get injections, only positional arguments. So scope, element, attrs would be more appropriate. – user1338062 Feb 25 '15 at 09:13
  • Is there any difference between `scope.$eval()` and javascript's native `eval()` function? – Bloke Sep 09 '15 at 14:32
  • 1
    @Bloke Here's a SO question you may find helpful. http://stackoverflow.com/questions/15671471/angular-js-how-does-eval-work-and-why-is-it-different-from-vanilla-eval – bingles Sep 09 '15 at 18:56
8

Create a scope on the directive that sets up bi-directional binding:

app.controller('ctrl', function($scope)
{
    $scope.list = {
        name: 'Test',
        collapsed: true,
        items: [1, 2, 3]
    };
});

app.directive('list', function()
{
    return {
        restrict: 'E',
        scope: {
            collapsed: '=',
            name: '=',
            items: '='
        },
        template:
            '<div>' +
                '{{name}} collapsed: {{collapsed}}' +
                '<div ng-show="!collapsed">' +
                    '<div ng-repeat="item in items">Item {{item}}</div>' +
                '</div>' +
                '<br><input type="button" ng-click="collapsed = !collapsed" value="Inside Toggle">' +
            '</div>'
    };
});

Then pass the options in as attributes:

<list items="list.items" name="list.name" collapsed="list.collapsed"></list>

http://jsfiddle.net/aaGhd/3/

noj
  • 6,740
  • 1
  • 25
  • 30
  • 2
    Definitely, this works! But I'd like also to be able to set the `collapsed` attribute with a boolean value, with `true` or `false` – Chris X Aug 02 '13 at 13:27
5

You can't pass strings true or false as the attribute value, and also support passing a scope property such as list.collapsed as the attribute value for two-way binding. You have to pick one way or the other.

This is because you can only specify one way to interpret the attribute's value in your directive when using an isolate scope.

I suppose you could use = in your diretive, and also check in your linking function if attrs.collapsable is set to true or false: if so, then you know a boolean value was passed, and if not, use the two-way data binding. But this is hacky.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
2

I know I'm like a year late on this, but you can actually do this by using the link function (https://docs.angularjs.org/guide/directive). The signature looks like this:

function link(scope, element, attrs) { ... } 

That attrs object will be filled out with the raw values passed in. So you could say if (attrs.collapsed == 'true') { ... } or some such.

Brandon Prudent
  • 313
  • 3
  • 8
  • 1
    That's not going to work. All values inside an attribute are of type string. So if you were to set collapsed="false", then check if (attrs.collapsed), it would be true, because the string "false" is truthy – Charlie Martin Jul 09 '14 at 21:36
  • Indeed. I updated the example with a simple 'true' string test. I think it does address the intent of the question. – Brandon Prudent Jul 10 '14 at 00:43
2

Since Angular 1.3 attrs.$observe seems to trigger also for undefined attributes, so if you want to account for an undefined attribute, you need to do something like:

link: function(scope, element, attrs) {
    attrs.$observe('collapsable', function() {
      scope.collapsable = scope.$eval(attrs.collapsable);
      if (scope.collapsable === undefined) {
        delete scope.collapsable;
      }
    });
  },
user1338062
  • 11,939
  • 3
  • 73
  • 67