Confused about Angularjs transcluded and isolate scopes & bindings
Asked Answered
J

3

55

I am struggling to understand the scope of models and their bindings in respect of directives which have limited scope.

I get that restricting the scope on a directive means that controller.$scope and directive.scope are no longer the same thing. However, I am confused about how the placing of models either within the directive template or in the html affects data binding. I feel I'm missing something very fundamental and to move on I need to understand this.

Take the following code (fiddle here: http://jsfiddle.net/2ams6/)

JavaScript

var app = angular.module('app',[]);
app.controller('Ctrl',function($scope){
});
app.directive('testel', function(){
    return {
        restrict: 'E',
        scope: {
            title: '@'
        },
        transclude: true,
        template:   '<div ng-transclude>'+
                    '<h3>Template title: {{title}}</h3>' +
                    '<h3>Template data.title:{{data.title}}</h3>' +
                    '</div>'
    }    
}); 

HTML

<div ng-app='app'>
    <div ng-controller="Ctrl">
        <input ng-model="data.title">
        <testel title="{{data.title}}">
            <h3>Transclude title:{{title}}</span></h3>
            <h3>Transclude data.title:{{data.title}}</h3>
        </testel>
    </div>
</div>

The model only updates {{title}} within the template, and {{data.title}} in the transclusion. Why not {{title}} in the transclusion nor {{data.title}} in the template?

Moving the input to within the transclusion like so (fiddle here: http://jsfiddle.net/eV8q8/1/):

<div ng-controller="Ctrl">
    <testel title="{{data.title}}">
        <input ng-model="data.title">
         <h3>Transclude title: <span style="color:red">{{title}}</span></h3>

         <h3>Transclude data.title: <span style="color:red">{{data.title}}</span></h3>

    </testel>
</div>

now means only transclude {{data:title}} gets updated. Why not either template {{title}} or {{data.title}}, nor transclude {{title}}?

And finally, moving the input to within the template, like so (fiddle here: http://jsfiddle.net/4ngmf/2/):

template: '<div ng-transclude>' +
            '<input ng-model="data.title" />' +
            '<h3>Template title: {{title}}</h3>' +
            '<h3>Template data.title: {{data.title}}</h3>' +
            '</div>'

Now means that only template {{data.title}} gets updated. Again, why not the other 3 bindings?

I hope there is something obvious staring me in the face and I'm missing it. If you get me to get this, I'll buy you a beer, or give you some points, or some other such thing. Many thanks.

Jard answered 20/5, 2013 at 15:34 Comment(2)
This is a big topic and I unfortunately don't have time to give too much detail before heading to work, but read Mark Rajcok's Scope/Prototypal Inheritance Guide. There are lots of diagrams, which helps, and he covers transclusion.Prostatectomy
@BrandonTilley Amazing article. many, many thanks for posting it. Just working my way through it. Be able to spend more time on it Tuesday.Jard
V
114

Your fiddles create three scopes:

  1. a scope associated with controller Ctrl, because of ng-controller
  2. a directive transcluded scope, because of transclude: true
  3. a directive isolate scope, because of scope: { ... }

In fiddle1, before we type anything into the text box we have the following:

enter image description here

Scope 003 is the scope associated with the controller. Since we didn't type into the textbox yet, there is no data property. In isolate scope 004, we see that a title property was created, but it is empty. It is empty because the parent scope doesn't have a data.title property yet.

After typing my title into the textbox, we now have:

enter image description here

Controller scope 003 now has a new data object property (which is why it is colored yellow), which has a title property now set to my title. Since isolate scope property title is one-way databound to the interpolated value of data.title, it also gets the value my title (the value is colored yellow because it changed).

The transcluded scope prototypically inherits from the controller scope, so inside the transcluded HTML, angular can follow the prototype chain and find $scope.data.title in the parent scope (but $scope.title doesn't exist there).

The isolate scope only has access to its own properties, hence only property title.

In fiddle2, before typing we have the same picture as in fiddle1.

After typing my title:

enter image description here

Notice where the new data.title property showed up -- on the transcluded scope. The isolate scope is still looking for data.title on the controller scope, but its not there this time, so its title property value remains empty.

In fiddle3, before typing we have the same picture as in fiddle1.

After typing my title:

enter image description here

Notice where the new data.title property showed up -- on the isolate scope. None of the other scopes have access to the isolate scope, so the string my title won't appear anywhere else.


Update for Angular v1.2:

With change eed299a Angular now clears the transclusion point before transcluding, so the Template title: ... and Template data.title: ... parts won't show up unless you modify the template such that ng-transclude is by itself, such as:

'<h3>Template title: <span style="color:red">{{title}}</span></h3>' +
'<h3>Template data.title: <span style="color:red">{{data.title}}</span></h3>' +
'<div ng-transclude></div>'

In the update below for Angular v1.3, this template change was made.


Update for Angular v1.3+:

Since Angular v1.3, the transcluded scope is now a child of the directive's isolate scope, rather than a child of the controller scope. So in fiddle1, before we type anything:

enter image description here

The pictures in this update are drawn with the Peri$scope tool, so the pictures are a bit different. The @ indicates we have an isolate scope property that uses the @ syntax, and the pink background means that the tool was unable to find an ancestor reference for the mapping (which is true, since we didn't type anything in to the textbox yet).

After typing my title into the textbox, we now have:

enter image description here

Isolate properties that use @ binding will always show the interpolated string result in the isolate scope after the @ symbol. Peri$scope was also able to find this exact string value in an ancestor scope, so it also shows a reference to that property.

In fiddle 2, before typing we have the same picture as in fiddle1.

After typing my title:

enter image description here

Notice where the new data.title property showed up -- on the transcluded scope. The isolate scope is still looking for data.title on the controller scope, but its not there this time, so its title property value remains empty.

In fiddle3, before typing we have the same picture as in fiddle1.

After typing my title:

enter image description here

Notice where the new data.title property showed up -- on the isolate scope. Even though the transcluded scope has access to the isolate scope via the $parent relationship, it won't look there for title or data.title -- it will only look in the controller scope (i.e., it will follow the prototypal inheritance), and the controller scope doesn't have these properties defined.

Vonnievonny answered 23/5, 2013 at 3:28 Comment(4)
@Mark how did you do these diagrams? Is there a tool?Keane
@mikea, I wrote a half-baked tool many months ago. It started out nice and clean, but it quickly became hackish as I started trying to add more and more use cases and features. A directive parses lots of Angular objects (some code was reused from Batarang) and it then sends the interesting info to a Python script that generates a GraphViz dot file which is rendered server side and then returned to the browser as an image.Vonnievonny
@mikea, I baked the other half of the tool and put it up on github under the name of Peri$scopeVonnievonny
@MarkRajcok it's a pity that your Peri$scope project seems abandoned, we could use such a tool to debug our application. I tried to use it to debug our AngularJS 1.6 app, but I got errors. Nevertheless, great work! ;)Karlis
J
25

After reading through all the answers presented, including Mark's fantastic schematics, this is my understanding of scope and it's inheritance per my question. I would appreciate comments on where this diagram falls down, in order I can update appropriately. I hope it simply provides a different view to what Mark has presented:

Scope inheritance

Jard answered 24/5, 2013 at 12:52 Comment(2)
Note that you can change how the scopes are created, e.g. to expose the directive's scope to its transcluded children.Chole
@Chole very helpful thx. documentation on angular used to be very limited. looks like things are improving! :)Jard
H
8

Well asked, btw! Hope my answer is as eloquent..

The answer has to do with how transcluded elements get their scope.

To summarize, you've got two scopes:

  1. The controller's scope, which has $scope.data.title. (Implicitly added by your input element)
  2. The directive's scope, which has $scope.title.

When you change the controller's $scope.data.title, directive's $scope.title also changes.

You've also got two sections of HTML, the transcluded and the template. What's happening is that the transcluded HTML is in the controller's scope, and the template HTML is in the directive's scope. So the transcluded HTML doesn't know anything about title, and the template scope doesn't know anything about data.title

This is actually exactly what Transclusion was intended for - to allow child elements of a directive keep their parent scope, in this case the controller's scope. By design, transcluded elements don't know that their in a directive, and so don't have access to the directive's scope.

Directive templates, on the other hand, will have access only to the directive's scope.

I've changed your code a bit to make the names a bit more clear (same functionality, though)

http://jsfiddle.net/yWWVs/2/

Hermetic answered 20/5, 2013 at 16:14 Comment(1)
thx for taking the time to answer. I feel I'm coming out of the murky water to something clearer. The article posted above by Brandon Tilley is also very helpful, so I'm digesting it alongside your answer in the hope everything is going to fall into place. I think when I make it to the transclusion part of Brandon's article, things will be much clearer. I take time to digest answers very slowly and deliberately so bear with me.Jard

© 2022 - 2024 — McMap. All rights reserved.