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.

2
  • 2
    This question appears to be off-topic because it is about code review. Commented Sep 19, 2013 at 14:03
  • 2
    Or code architecture. I think it's a good question. Commented Sep 1, 2015 at 22:31

5 Answers 5

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Doug T. I was going off of this SO post, but I probably only understood a quarter of it.
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.
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.

2 Comments

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.
Then go for isolate scope, see Doug's answer.
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();
            });
        }
    };
}]);

4 Comments

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 -- my JS skills are not the best, so I may have missed something though.
@DavidFaivre You need to initialize it in the controller like $scope.counter={}; $scope.counter.val = 0
if you're interested, see this 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.
@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.
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.

4 Comments

how can i access data[1] in my directive controller to make HTTP get call
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; ... }
@n.bharath I wrote this out with the use of components: plnkr.co/edit/4wOdA3OYkxS9vXAWfSxZ?p=preview
From <my-component name='data.names[0]'></my-component> access name in controller based on name value then do http get call and bind response to template ** "<div>Your name is : {{$ctrl.response }}</div>"**
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();
                });
            }
        };
    }
);

2 Comments

Can you explain how this is an answer to the question?
While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.