Moving Data In An AngularJS Directive

Wednesday, September 11, 2013

This plunkr demonstrates a few different scenarios and capabilities when moving data between a model and a directive. Like other places in Angular JS there are quite a number of different approaches that work, which grants a lot of flexibility but also leads to some confusion.

The example uses a simple controller.

module.controller("TestController", function($scope){
    
    $scope.magicValue = 1; 
     
    $scope.increment = function(){
      $scope.magicValue += 1;
    };
    
});

The markup for the sample displays the controller’s magicValue, and provides a button to change the value. There are also 5 versions of a valueDisplay directive.

<div ng-controller="TestController">
  Controller: {{magicValue}}
  <button ng-click="increment()">Click</button>

  <ol>
    <li><value-display1 value="magicValue"></value-display1></li>

    <li><value-display2 value="magicValue"></value-display2></li>

    <li><value-display3 value="{{magicValue}}"></value-display3></li>

    <li><value-display4 value="magicValue"></value-display4></li>
       
    <li><value-display5 value="{{magicValue}}"></value-display5></li>
  </ol>
</div>

The first version of valueDisplay will put the value on the screen, but will never update the screen if the magic value changes. This approach is only reasonable for static data since the directive reads the value directly from the controller’s scope (using the name specified in the attribute), places the value in the element, then never looks back.

module.directive("valueDisplay1", function () {
    return {
    restrict: "E",
       link: function (scope, element, attrs) {
           element.text(scope[attrs.value]);
       }
   };
});

The second version of valueDisplay uses a $watch to update the screen as the model value changes. Notice the attribute value (value="magicString")  is still working like an expression to retrieve and watch data directly from the controller’s scope.

module.directive("valueDisplay2", function () {

     return {
        restrict: "E",
        link: function (scope, element, attrs) {
            scope.$watch(attrs.value, function(newValue) {
                element.text(newValue);
            });
        }
    };

});

The third version is a slight variation that will read and observe data in the value attribute itself instead of going to the controller. There are a few differences to note. First, the directive will always retrieve the value as a string. Secondly, the view has to use interpolation to place the proper value into the attribute (i.e. value="{{magicValue}}"). Finally, $observe is used instead of a scope watch since the directive wants to watch changes on the attribute, not the controller scope.

module.directive("valueDisplay3", function () {
 return {
    restrict: "E",
    link: function (scope, element, attrs) {
        attrs.$observe("value", function (newValue) {
            element.text(newValue);
        });
    }
  };
});

The fourth version moves to using an isolate scope to detach itself from the parent scope. Two-way data binding is setup between the directive's isloated scope and the parent model using using "=", so the framework will take care of pushing and pulling data between the directive’s scope and the parent scope. The view still chooses the model property to use with the value attribute in the markup (value="magicValue"), and displays the value using a template (which will automatically watch for changes).

module.directive("valueDisplay4", function () {
     return {
        restrict: "E",
        scope: {
            value: "="
        },
        template: '{{value}}',
    };
});

The final version of valueDisplay uses attribute binding ("@"), which also works in both directions and will update the view as the model value changes (and vice-versa, if we had an input). 

module.directive("valueDisplay5", function () {
    return {
        restrict: "E",
        scope: {
              value: "@"
        },
        template: '{{value}}',
    };
});

In the end, there are even more options available (we didn’t even look at executing expressions to move data), but that can be a topic for a future post. I currently tend to use option 4 for binding complex objects from the model to a directive, and option 5 for moving simple customization values like headings, labels, and titles.


Comments
gravatar robinhoody Wednesday, September 11, 2013
Thanks Scott, it's interesting to see what other people use. I'm with you, option four for complex. It's taken me a while to get to that stage though, a lot of head banging against the wall and "why won't this value update itself?" style questions lol.
gravatar Jesse Gavin Wednesday, September 11, 2013
This is one of the clearest explanations I have ever seen on this topic. Thank you.
gravatar Evgeniy OZ Wednesday, September 11, 2013
I prefer isolated scope. $watch and $observe looks dirty :)
gravatar Jeff Benson Wednesday, September 11, 2013
$watch and $observe are more powerful though if you need to respond in some way to value changes (beyond merely displaying it).
gravatar Tushar Sharma Friday, September 13, 2013
Hey Guys..Sorry to go off-road.. but i am stuck in a serious problem. Can anybody help me out ???
gravatar srigi Tuesday, September 17, 2013
Most natural variation isn't listed here - directive using scope hierarchy. If directive is in HTML under Controller element, it's scope is prototypaly inherited from Controller's scope. That way you can simply access scope.magicValue in your directive.
gravatar Chris Shepherd Tuesday, September 17, 2013
Fab explanation - and much needed. Often I find that trying to look for help for debugging an interaction between a directive and a controller, I find someone doing it using a different method as outlined above. This is really confusing because it makes me think that I'm doing it the wrong way, when in fact, there's pros and cons, and they all work in different situations. This is brilliantly clear.
gravatar GFoley83 Tuesday, September 17, 2013
Great breakdown, really helps to see the different approaches side by side. "valueDisplay5" is a one-way binding only though (Model -> View); not two-way as noted.
gravatar devtools.korzh.com Wednesday, September 18, 2013
Thanks for showing new approaches. Really helpful! I also found a lot of useful info at DevtoolsKorzh. Thanks again!
gravatar Husniddin Thursday, September 19, 2013
thank you very much. I learned distinction $watch and $observe
gravatar J. Bruni Friday, September 27, 2013
In general, I avoid isolated scopes, as ngModel does not play nice with them. I only go for isolated scope if it is really required. Otherwise, if it is possible to achieve something reasonably without isolated scope, I'd prefer it.
gravatar Kabby Tuesday, October 22, 2013
Thank you, this was an excellent post with clear examples. I am looking forward to the post you mentioned about executing expressions to move data - I am struggling through that right now.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!