29

I'm giving a first try at AngularJS custom directives.

I'm having trouble using (or understanding ...) the isolated scope in the link function of the directive.

Here is the code of this part of my app :

view.html

...
<raw-data id="request-data" title="XML of the request" data="request">See the request</raw-data>
...

request is a variable published in the scope of the viewCtrl that contains the xml-string of a request.

rawData.js

directives.directive('rawData', function() {

    return {
        restrict : 'E',
        templateUrl : 'partials/directives/raw-data.html',
        replace : true,
        transclude : true,
        scope : {
            id : '@',
            title : '@',
            data : '='
        },
        link : function($scope, $elem, $attr) {
            console.log($scope.data); //the data is correclty printed
            console.log($scope.id); //undefined
        }
    };
});

raw-data.html

<div>
    <!-- Button to trigger modal -->
    <a href="#{{id}}Modal" role="button" class="btn" data-toggle="modal" ng-transclude></a>

    <!-- Modal -->
    <div id="{{id}}Modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="{{id}}Modal" aria-hidden="true">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
            <h3 id="myModalLabel">{{ title }}</h3>
        </div>
        <div class="modal-body">
            <textarea class="input-block-level" rows="10">{{ data }}</textarea>
        </div>
        <div class="modal-footer">
            <!-- <button class="btn" ng-click="toggleTagText('')">{{'cacher'}} l'image</button> -->
            <button class="btn btn-primary" data-dismiss="modal" aria-hidden="true">Fermer</button>
        </div>
    </div>
</div>

I don't understand why the ID is correclty shown when the modal pops, but when I try to console.log() it, its value is undefined.

Maybe i'm wrong with the isolated scope value (= and @).

Thank you for reading. :)

pdegand59
  • 12,776
  • 8
  • 51
  • 58
  • This is total guesswork, but is "id" special somehow because "id" a basic part of the DOM and Angular won't map it into your directive properly for that reason? Have you tried renaming that attribute to see if it helps it work better? – blaster Jun 14 '13 at 15:03
  • Helps a lot if you can put up a fiddle (jsfiddle.com/plnkr.co). – abject_error Jun 14 '13 at 15:04
  • @blaster: Same is was happening with the "title" and it's not a basic prop of the DOM. :) – pdegand59 Jun 14 '13 at 15:49
  • @abject_error: I will think of it for my next question. Appologies ! ;) – pdegand59 Jun 14 '13 at 15:50

4 Answers4

36

Isolate scope properties bound with @ are not immediately available in the linking function. You need to use $observe:

$attr.$observe('id', function(value) {
   console.log(value);
});

Your template works properly because Angular automatically updates isolate scope property id for you. And when it does update, your template automatically updates too.

If you are just passing strings, you can simply evaluate the values once instead of using @ binding:

link: function($scope, $elem, $attr) {
    var id    = $attr.id;
    var title = $attr.title
    console.log(id, title);
}

However, in your case, since you want to use the properties in templates, you should use @.

If you weren't using templates, then @ is useful when attribute values contain {{}}s – e.g., title="{{myTitle}}". Then the need to use $observe becomes more apparent: your linking function may want to do something each time the value of myTitle changes.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Thanks for theses explanations. I also tried to wrap the `console.log()` with `$timeout(function() { ... }, 0, true);` and the value of `$scope.id` was correctly set. As you said, I thinked about using the `$attr` object, but as I needed theses values in the template, the '@' was needed. Now everything is working as intended! :) – pdegand59 Jun 14 '13 at 15:44
  • Would you use $observe for two way binded attributes? I'm facing the same problem of undefined when I log it, but it works inside the template. The attributes are model properties (primitives and objects) from the parent scope, so I've been trying '=' binding. How do you get console.log to correctly log the value? – CMCDragonkai Aug 20 '13 at 16:44
  • By the way the model property is of course populated from an AJAX request, so it's empty when it starts. – CMCDragonkai Aug 20 '13 at 16:51
  • 1
    @CMCDragonkai, I suggest posting what you've tried as a new question. You likely need to use `angular.copy()` when $http/AJAX returns the results, but I would need to see the code. With `=` you would use `$watch()`, not `$observe()`. – Mark Rajcok Aug 20 '13 at 17:16
  • This answer helped me alot: http://stackoverflow.com/questions/14876112/difference-between-observers-and-watchers But I also added an extra comment. – CMCDragonkai Aug 21 '13 at 03:12
  • Can you `$attr` in link when you need to execute the `scope` with a function say `submitFunction : '&'`? – Neel Feb 21 '17 at 08:40
10

This is intentional and has to do with compilation order and the difference between '@' and '='.

Some excerpts from this Google Groups discussion with input from Misko:

@ and = do very different things. One copies the attribute value (which may be interpolated) and the other treats the attribute value as an expression.

@attrs are not $interpolated until later, so they are not available at link time. If you want to do something with them in the link function you either need to $interpolate yourself manually

Community
  • 1
  • 1
Dan
  • 59,490
  • 13
  • 101
  • 110
0

well, none of the answers above actually mentioned one key aspect, even with '=', it doesn't seem to me you can access the scope inside link function directly like the following,

scope: {
    data: '=',
},
link: function(scope, elem, attrs) {
    console.debug(scope.data); // undefined

but you can access the scope in the internal function,

link: function(scope, elem, attrs) {
    scope.saveComment = function() {
        console.debug(scope.data);

so it seems to me there might be a time lag in when scope.data can be available.

windmaomao
  • 7,120
  • 2
  • 32
  • 36
0

You can visit the JSFiddle created by me here: http://jsfiddle.net/7t984sf9/5/:

link: function($scope, $elem, $attr) {
    console.log($scope.onewayObject);
    $attr.$observe('onewayObject', function(value) {
         console.log(value);
    });
}

Or some more detailed explanation here: What is the difference between & vs @ and = in angularJS

Community
  • 1
  • 1
Joy
  • 9,430
  • 11
  • 44
  • 95