AngularJS Sticky Notes Pt 2 – Isolated Scope

Welcome to Part 2 of the AngularJS Sticky Notes series!

In this blogpost I am going to talk about “isolated” scope as it relates to directives. Directives are one of the most powerful features of AngularJS and yet it can be one of the most confusing aspects of it as well. I believe that part of what makes directives hard to understand are the nuances surrounding scope.

Scope by default inherits from its parent scope, but this may not be desirable behavior, especially if you are building a re-usable widget. It is important that directives cannot accidentally read or write properties in the parent scope. This is where isolated scope comes in. Isolated scope does not prototypically inherit from the parent scope. It is essentially an island unto itself. So, let’s cover the basics of isolated scope and then we will talk about how I used it in the sticky notes application.

I have prepared an example to illustrate what I am going to be talking about, and you can find it here: http://jsfiddle.net/simpulton/SPMfT/

You can essentially interact with isolated scope in three ways:

1. Attributes

You can bind a isolated scope property to a DOM attribute. This sets up a one-way databinding from the parent scope to the isolated scope. If the parent scope changes, the isolated scope will reflect that change, but not the other way around. You wire this up using an @ symbol in your scope property definition in the directive.

Important note: I am binding the attribute attributeFoo to a isolated property called isolatedAttributeFoo. In most cases it is not necessary to have the properties be different names but I only did this to call out the difference between parent scope and isolated scope.

1
2
3
4
5
6
7
.directive('myComponent', function () {
    return {
        scope:{
            isolatedAttributeFoo:'@attributeFoo',
        }        
    };
})

If both my isolated property and parent property were attributeFoo, then I would simply have to do this:

1
2
3
4
5
6
7
.directive('myComponent', function () {
    return {
        scope:{
            attributeFoo:'@',
        }        
    };
})

One quick note about attribute expressions, the result is always a string since DOM elements are always strings. That is why I am using double curly braces so that string interpolation can happen, as follows:

1
<my-component attribute-foo="{{foo}}"></my-component>

Well what happens if you want two-way databinding between parent and isolated scope? Easy enough!

2. Bindings

This works almost exactly like the previous example except that you use an = sign instead of an @ symbol, as follows:

1
2
3
4
5
6
7
.directive('myComponent', function () {
    return {
        scope:{
            isolatedBindingFoo:'=bindingFoo',
        }        
    };
})

And if isolatedBindingFoo changes then it will update the property that exists in bindingFoo. The same special note applies to this example as well. If my properties in parent scope and isolated scope have the same name then the syntax is simplified.

1
2
3
4
5
6
7
.directive('myComponent', function () {
    return {
        scope:{
            bindingFoo:'=',
        }        
    };
})

But what if I want to call a function on the parent scope from isolated scope? This is a bit more tricky, but it is not going to make a grown man cry over it.

3. Expressions

The simple part first – to wire this up to call an expression on the parent scope from the isolated scope you use the & symbol, like so:

1
2
3
4
5
6
7
.directive('myComponent', function () {
    return {
        scope:{
            isolatedExpressionFoo:'&'
        }        
    };
})

And now when I call isolatedExpressionFoo in my isolated scope it serves as a wrapper to whatever I defined in my directive definition. In this case it is updateFoo. If you want to pass data through the function wrapper, you do that by passing your isolated variables through via an object map. This is why my function call looks like this isolatedExpressionFoo({newFoo:isolatedFoo}).

1
2
<input ng-model="isolatedFoo">
<button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">Submit</button>

And then! updateFoo gets called and does something incredibly clever.

1
2
3
4
5
.controller('MyCtrl', ['$scope', function ($scope) {
    $scope.updateFoo = function (newFoo) {
        $scope.foo = newFoo;
    }
}]);

So, that is our whirlwind tour of isolated scope!

The Application!

This will actually be fairly brief since we covered the nuts and bolts of isolated scope already.

For reference purposes here is the fiddle:
http://jsfiddle.net/simpulton/VJ94U/

The application has isolated scope in two places, the first being the myNotebook directive, and the second being the myNote directive. What is interesting is that I am setting isolated scope within isolated scope. I am binding notes from NotebookCtrl to the myNotebook directive and from there I am looping over notes and binding note from myNotebook to the myNote directive. The reason I am using two-way databinding instead of an attribute expression is because I am dealing with objects and so string interpolation would not be optimal.

The other problem that I faced was I wanted to be able to initiate the delete process from the note directive without having to reach out of its isolated scope. Essentially I am having to pass through two layers of functions to get back up to the scope, but I managed to do it while keeping my hands in the ride at all times. From the note directive, I call delete which is a wrapper that calls onDelete which is a wrapper that calls deleteNote. Tada!

Conclusion

The majority of this post was spent breaking down isolated scope and how it works. Once I realized that the syntax had been simplified to basically three options, the lights just kind of went on. From there it was fairly self-evident why isolated scope in AngularJS Sticky Notes was wired up the way it was. Two-way binding and function wrappers all the way down. I am still in awe that isolated scope worked so well even when I went all Inception on it and started creating isolated scope within isolated scope.

A special thanks to John Lindquist, Vojta Jína and Ken Slachta for reviewing my post and examples before I released it to the wild.

Resources

AngularJS Sticky Notes Fiddle
http://jsfiddle.net/simpulton/VJ94U/

AngularJS Sticky Notes Repository
https://github.com/simpulton/angular-sticky-notes

AngularJS Directive Documentation
http://docs.angularjs.org/guide/directive

AngularJS Mailing List
https://groups.google.com/forum/?fromgroups#!forum/angular

AngularJS Sticky Notes Pt 2 – Isolated Scope

31 Responses

  1. Great post, thanks a lot!
    One thing, it looks like in the code, showFoo was renamed to updateFoo. I was very confused there for a moment! :)

    J

    javier Abanses July 12, 2012 at 12:27 pm #
  2. Ah yes… sometimes things slip through the cracks after 100 revisions. Thanks Javier!

    simpulton July 12, 2012 at 12:49 pm #
  3. Nice one, good job :)

    Bretto July 13, 2012 at 1:55 am #
  4. Thanks for going into such great detail in both part 1 & 2. I think this is WAYYYY over my head. So rather than ask a million questions on your code samples and jsfiddle, could you tell me where it is you learned some of the ropes regarding understanding the concepts of scope, controllers, directives, etc.?

    I come from a Flex/as3 background and while I assume some of these concepts have matching concepts in the Flash realm, I am having a hard time making sense of some of the syntax and concepts as they relate to javascript.

    I will see you on the Angular mailing list too :) Thanks.

    jwopitz July 14, 2012 at 6:14 pm #
  5. I just realized that the Angular site has received a complete makeover w/ all new docs and tutorials. Please pardon my last comment.

    jwopitz July 14, 2012 at 6:41 pm #
  6. @jwopitz good questions :D I found AngularJS to be a very natural transition from Flex.

    Two-way data binding and custom components instantiated via a declarative markup etc were old friends to me.

    I recommend JavaScript: The Good Parts and JavaScript Patterns for ramping on JavaScript as a whole. I have also been through the AngularJS docs front to back at least three times which was extremely helpful.

    And! Most importantly, write a LOT of code. Just jump into jsFiddle and starting playing with ideas.

    I hope this helps!

    simpulton July 14, 2012 at 6:50 pm #
  7. Sections 1&2 are really great and clear.
    Section 3 is `very` confusing. Honestly I did not understand it… even after readying it three (3) times.

    IMHO, Using scope:{ delete:’&’ } to provide access to parent delete feature is too obtuse. This is a great case for dispatching an event and attaching an `on( )` handler at the parent level. I don’t think the AngularJS option is viable architecture approach.

    Regardless, I think your articles are a fantastic resource for the AngJS community.

    Thanks again,
    ThomasB

    Thomas Burleson July 14, 2012 at 10:41 pm #
  8. Thomas, you are the only person who can talk to me like that and get away with it! Haha

    simpulton July 14, 2012 at 11:01 pm #
  9. There is a bug, when deleting notes. You can’t create the id only based by the length of the array of notes. If you delete a note, which is not last, and then add a new note, you will have two notes of the same id, which will cause deleting both of them, when you only want to delete one.

    I have fix it. Now it works fine. http://jsfiddle.net/VJ94U/171/

    Anyway, great material! Your example helped me fix my own bug in my app and I learned some more stuff about directives.

    David Krutky July 17, 2012 at 9:15 am #
  10. Thanks David! As soon as I get a chance I will work your change into the code… appreciate the fix

    simpulton July 17, 2012 at 6:14 pm #
  11. Just to clarify
    There are two different deleteNotes defined and referenced. They are not the same function

    The one referenced in this line is the deleteNote defined in the my-notebook directive controller

    defined as
    $scope.deleteNote = function (id) {
    $scope.ondelete({id:id});
    }

    This one is the deleteNote defined in the app controllers

    Defined as
    $scope.deleteNote = function (id) {
    notesService.deleteNote(id);
    };

    It would have been less confusing if you had used a different name for these two functions

    Samuel Smith October 31, 2012 at 11:22 pm #
  12. Whoops forgot to escape the html

    Samuel Smith October 31, 2012 at 11:25 pm #
  13. Samuel Smith October 31, 2012 at 11:27 pm #
  14. my-note class=”span2 thumbnail” delete=”deleteNote(note.id)” note=”note”

    my-notebook notes=”getNotes()” ondelete=”deleteNote(id)”

    Samuel Smith October 31, 2012 at 11:28 pm #
  15. I had a n00bi question, how did you include the “partials/notebook-directive.html” in the JsFiddle and how can I see that file?

    Arminio January 22, 2013 at 4:18 am #
  16. Hi Arminio — A little jsfiddle hackery was used here… the partial is actually inside this script tag in the HTML and then we just reference like an external file.

    simpulton January 22, 2013 at 5:15 pm #
  17. Samuel — totally valid point. I prefer to think of isolated scope as it pertains to expressions as simply a pass through function. So I tend to use the same name so that the mapping is explicit.

    deleteNote [internal] just hands off to deleteNote [external] but I would be the first to admit that it is a matter of preference.

    simpulton January 22, 2013 at 5:20 pm #
  18. you should restrict the directive to E

    1
    2
    3
    4
    5
    6
    7
    8
    .directive('myComponent', function () {
        return {
            restrict: "E",
            scope:{
                isolatedAttributeFoo:'@attributeFoo',
            }        
        };
    })
    nur mohammed rony February 17, 2013 at 4:14 am #
  19. Why ‘should’? I think there are cases where it makes sense to restrict a directive but that really reduces portability. In my example, I am using as an element but there is nothing keep you from using it as an attribute or even a class. THAT I think is really cool!

    simpulton February 17, 2013 at 5:35 am #
  20. Thanks for explaining! I have to say, though, the example here for part 3, Expressions, seems to be missing something. Where is anything ever connecting to updateFoo?

    Alan Hogan May 21, 2013 at 10:43 pm #
  21. Hi Alan –
    It gets hooked up via isolated-expression-foo=”updateFoo(newFoo)” basically when you call isolated-expression-foo via ng-click=”isolatedExpressionFoo({newFoo:isolatedFoo})” it serves as a proxy to call updateFoo (or any other method you put in there). Does this make sense?

    simpulton May 21, 2013 at 10:49 pm #
  22. @simpulton To me, it looks like `newFoo` and `updateFoo` should both just be `updateFoo`.

    Your article reads, “when I call isolatedExpressionFoo in my isolated scope it serves as a wrapper to whatever I defined in my directive definition. In this case it is updateFoo.” Now, when you say “directive definition,” I figure you could either mean to refer to the .directive() call or to the ng-click attribute in the template, but *neither* mentions `updateFoo`!

    Alan Hogan May 25, 2013 at 12:55 am #
  23. @alan Ah yes I see the problem… assumptions will be the death of us. At the time I wrote that with the assumption that readers were going to follow along with the fiddle that I reference at the beginning of the article which is http://jsfiddle.net/simpulton/SPMfT/. I failed to call that out in the article. My apologies.

    1. updateFoo exists on MyCtrl (the parent scope) and takes an argument of newFoo a la $scope.updateFoo = function (newFoo)
    2. In the fiddle I am instantiating the directive via my-component attribute-foo=”{{foo}}” binding-foo=”foo” isolated-expression-foo=”updateFoo(newFoo)”
    3. In my directive definition object, I am defining the expression isolated scope via isolatedExpressionFoo:’&’
    4. In the directive, I am calling isolatedExpressionFoo via ng-click=”isolatedExpressionFoo({newFoo:isolatedFoo})”. NOTE! For whatever reason you have to pass variables via a param object when using expression scope.
    5. And thus the chain is complete, isolatedExpressionFoo fires in the directive passing in the param object of {newFoo:isolatedFoo} which gets sent to isolated-expression-foo (note the snake-case) in the HTML which fires off whatever function it is bound to. In this case, it is updateFoo and it accepts {newFoo:isolatedFoo} as the newFoo parameter.

    In conclusion, I totally admit I could have elaborated on expression isolated scope better and I also admit that it is by nature a bit confusing already! If this still does not make sense, hit me up and we can chat. Holla!

    simpulton May 25, 2013 at 1:12 am #
  24. Ah yes! In your example on jsFiddle, you define `isolated-expression-foo=”updateFoo(newFoo)`. And then it all makes sense.

    I’m truly grateful for and impressed with your follow-up here. Thank you!

    Alan Hogan May 25, 2013 at 4:01 am #
  25. @simpulton . I have a question about the part 2. If the parent scope doesn’t have a property, then the parent will auto create it? I have an example here: http://jsfiddle.net/yougen/mvYrJ/4/
    Even if the controller does not have “network” property, but since in the directive, I declare the scope:{isoNetwork:’=network’}, when I debug into the controller, I find the controller scope HAVE “network” property.

    yougen August 28, 2013 at 7:40 am #
  26. Yes that is correct. Because you are binding the two properties together, when one gets set… it populates the other. It is implicit scope property creation like ng-model. Does this make sense?

    simpulton August 28, 2013 at 8:09 am #
  27. Nice article, “& prop” was my big misunderstanding, now I see it as a pointer to a parent scope function. thx.

    darul November 6, 2013 at 4:07 pm #
  28. Thanks.

    Wasn’t aware that a directive that did not define a template or templateUrl will assume as content the html source it is wrapping. Cool.

    George November 18, 2013 at 10:27 pm #
  29. I came to this page because I was having issue with the expression binding. After I tried this fiddle i realized it has same problem: http://jsfiddle.net/simpulton/SPMfT/ Basically if you type ‘abcd’ into the “Attribute/set” input box, then click submit, all the values are set to empty! I was expecting clicking the submit will set the parent foo (and others) to be ‘abcd’. Am I interpreting it wrong?

    tjworks December 5, 2013 at 4:38 am #
  30. I am not seeing that behavior. What SHOULD happen is when you type ‘abcd’ in the Expression input box and hit Submit… it will call updateFoo on the MainCtrl and update $scope.foo which will update all sorts of things as a result.

    simpulton December 5, 2013 at 4:49 am #
  31. The fiddle example doesnt work as of 1.2 due to the new isolated scope fix! Please update the article to reflect this new change!

    Le Duc Duy January 30, 2014 at 2:41 pm #

Leave a Reply