15

If I have an AngularJS directive without a template and I want it to set a property on the current scope, what is the best way to do it?

For example, a directive that counts button clicks:

<button twoway="counter">Click Me</button>
<p>Click Count: {{ counter }}</p>

With a directive that assigns the click count to the expression in the two way attribute:

.directive('twoway', [
'$parse',
  function($parse) {
    return {
      scope: false,
      link: function(scope, elem, attrs) {
        elem.on('click', function() {
          var current = scope.$eval(attrs.twoway) || 0;
          $parse(attrs.twoway).assign(scope, ++current);
          scope.$apply();
        });
      }
    };
  }
])

Is there a better way to do this? From what I've read, an isolated scope would be overkill, but do I need a child scope? And is there a cleaner way to write back to a scope variable defined in the directive attribute other than using $parse. I just feel like I'm making this too difficult.

Full Plunker here.

Doug T.
  • 64,223
  • 27
  • 138
  • 202
David Faivre
  • 2,302
  • 3
  • 23
  • 25

5 Answers5

33

Why is an isolate scope overkill? its pretty useful for exactly this kind of thing:

  scope: {
     "twoway": "=" // two way binding
  },

This is a pretty idiomatic angular solution to this problem, so this is what I'd stick with.

Doug T.
  • 64,223
  • 27
  • 138
  • 202
  • Doug T. I was going off of [this SO post](http://stackoverflow.com/questions/14914213/when-writing-a-directive-how-do-i-decide-if-a-need-no-new-scope-a-new-child-sc), but I probably only understood a quarter of it. – David Faivre Sep 18 '13 at 21:53
  • 40
    The critical addition to this answer is that the variable that you pass into this directive, e.g. `counter`, **must** be an object, e.g. `obj.counter` otherwise you won't get a proper reference to update the parent scope. – morloch Sep 29 '14 at 06:16
25

I'm surprised no one has mentioned ng-model, the default directive for doing two-data binding. Maybe it's not so well known, but the linking function has a fourth parameter:

angular.module('directive-binding', [])
  .directive('twoway', 
      function() {
        return {
          require: '?ngModel',
          link: function(scope, elem, attrs, ngModel) {
            elem.on('click', function() {
              var counter = ngModel.$viewValue ? ngModel.$viewValue : 0
              ngModel.$setViewValue(++counter);
              scope.$apply();
            });
          }
        };
      }
    );

On your view

<button twoway ng-model="counter">Click Me</button>
<p>Click Count: {{ counter }}</p>

The fourth parameter is an API for the ngModelController, which has many uses for handling (parsing and formatting, for instance) and sharing data between a directive and a scope.

Here's the updated Plunker.

jjperezaguinaga
  • 2,472
  • 14
  • 20
  • I was hoping to model the directive syntax after something like ngshow/nghide that takes the model property directly in its attribute value. I appreciate the implementation using ngModel though -- its something I haven't explored to deeply yet. – David Faivre Sep 18 '13 at 22:08
  • 1
    Then go for isolate scope, see Doug's answer. – jjperezaguinaga Sep 18 '13 at 22:17
2

You can definitely simplify it a bit like this without using $parse

angular.module('directive-binding', []).directive('twoway', [function () {
    return {
        scope: false,
        link: function (scope, elem, attrs) {
            elem.on('click', function () {
                scope[attrs.twoway] = scope[attrs.twoway] == null ? 1 : scope[attrs.twoway] + 1;
                scope.$apply();
            });
        }
    };
}]);
zs2020
  • 53,766
  • 29
  • 154
  • 219
  • 1
    This doesn't seem to work if you use a "nested" object in your model. For example, if I used "counter.val" instead of "counter", this doesn't seem to work. [Here's a Plunker](http://plnkr.co/edit/AvTTgW?p=preview) -- my JS skills are not the best, so I may have missed something though. – David Faivre Sep 18 '13 at 22:03
  • @DavidFaivre You need to initialize it in the controller like `$scope.counter={}; $scope.counter.val = 0` – zs2020 Sep 18 '13 at 22:23
  • if you're interested, see [this](http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key) on accessing "nested" objects by string. That's the underlying issue with this solution -- that you'd have to write some sort of parser. The $parser service, or the nested scope mapping, handles that for us in the Angular world. – David Faivre Sep 19 '13 at 12:08
  • @DavidFaivre You didn't mention you want to use nested object in your original question. When you ask question, make sure you ask something clear. – zs2020 Sep 19 '13 at 14:02
2

A great way to apply two way binding is to use directive components. Here is my solution. It allows use of ng-repeat and expandable data binding.

View Plunker

HTML

<body ng-controller='MainCtrl'>  
    Data: {{data}}
    <hr>
    <mydirective name='data[0]'></mydirective>
    <hr>
    <mydirective name='data[1]'></mydirective>
</body>

Controller

app.controller('MainCtrl', function($scope) {
  $scope.data = [];
  $scope.data[0] = 'Marco';
  $scope.data[1] = 'Billy';
});

Directive

app.directive("mydirective", function(){
    return {
        restrict: "EA",
        scope: {name: '='},
        template: "<div>Your name is : {{name}}</div>"+
        "Change your name : <input type='text' ng-model='name' />"
    };
});

In the case of the counter, it can be done using the same method.

BuffMcBigHuge
  • 579
  • 5
  • 8
  • how can i access **data[1]** in my directive controller to make **HTTP get** call – bhaRATh Mar 28 '19 at 08:01
  • You can add `{controller : myDirectivecontroller}` to your directive as part of the return object, and have it manipulate your data in the directive controller. I suggest keeping all RESTful requests in a Service, and have the Service referenced in the controller, like this: `function myDirectivecontroller(ServiceName) { var ctrl = this; ... }` – BuffMcBigHuge Mar 29 '19 at 12:39
  • @n.bharath I wrote this out with the use of components: http://plnkr.co/edit/4wOdA3OYkxS9vXAWfSxZ?p=preview – BuffMcBigHuge Mar 29 '19 at 13:27
  • From **** access **name** in controller based on **name** value then do **http get** call and bind response to **template** ** "
    Your name is : {{$ctrl.response }}
    "**
    – bhaRATh Apr 02 '19 at 06:51
0

Change template to:

<button twoway bind="counter">Click Me</button>
<p>Click Count: {{ counter.val }}</p>

and directive to:

.directive('twoway',
    function() {
        return {
            scope: {
                localValue: '=?bind'
            },
            link: function(scope, elem, attrs) {
                scope.localValue = {
                    val: 0
                };
                elem.on('click', function() {
                    scope.localValue.val = scope.localValue.val + 1;
                    scope.$apply();
                });
            }
        };
    }
);
brta
  • 11
  • 2