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.
.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:
.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:
<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:
.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.
.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:
.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}).
<input ng-model="isolatedFoo"> <button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">Submit</button>
And then! updateFoo gets called and does something incredibly clever.
.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
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
Ah yes… sometimes things slip through the cracks after 100 revisions. Thanks Javier!
Nice one, good job 🙂
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.
I just realized that the Angular site has received a complete makeover w/ all new docs and tutorials. Please pardon my last comment.
@jwopitz good questions 😀 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!
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, you are the only person who can talk to me like that and get away with it! Haha
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.
Thanks David! As soon as I get a chance I will work your change into the code… appreciate the fix
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
Whoops forgot to escape the html
“
“
my-note class=”span2 thumbnail” delete=”deleteNote(note.id)” note=”note”
my-notebook notes=”getNotes()” ondelete=”deleteNote(id)”
I had a n00bi question, how did you include the “partials/notebook-directive.html” in the JsFiddle and how can I see that file?
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.
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.
you should restrict the directive to E
.directive('myComponent', function () {
return {
restrict: "E",
scope:{
isolatedAttributeFoo:'@attributeFoo',
}
};
})
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!
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?
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 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 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!
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!
@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.
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?
Nice article, “& prop” was my big misunderstanding, now I see it as a pointer to a parent scope function. thx.
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.
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?
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.
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!
Great post, first one to explain directives and isolated scope without my head exploding 😀
Thank You Thank You Thank you!!!
I needed inception and you gave me a clean way of doing it. Was stuck for hours trying to figure it out.
This solution is cleaner than any whistle I’ve ever seen 🙂
Hi, there, in your fiddle program there are at least 2 mistakes: one is you create service, but you use factory method, so change the .service to .factory, another one is that parameter “elem” in link function is already jQuery wrapped, so no need $(‘elem’). One more thing is that in the controller definition of directive if it is right to call function from directive isolated scope? The scopes in directive definition is very confused to me. I know the link function and template in directive use the isolated scope (if exist), but how about controller?
Rookie mistakes! 😀 Cleaning up my old posts is in my backlog of things to get to. Thanks for pointing those out.
I recommend segregating your imperative logic into your directive’s controller whether it is isolated or not. This makes it easier to test the logical structures of a directive because they are in one place i.e. the controller. The controller and the link function within a directive share the same scope object whether it is isolated or not. Does this make sense?
I tried to convert your fiddle to ctrl as syntax and switched the angular version, but can’t get the directive attributes to bind to the ctrl scope.
http://jsfiddle.net/SPMfT/304/
Any thoughts would be amazing.
I’ve updated your Fiddle to use Angular 1.2 and Controller As syntax.
http://plnkr.co/edit/nUXWrj4yzypaQmtJShl9?p=preview
In 1.2 isolated scope variables can’t be used directly in the DOM, they have to be in the template of the directive. I made sure mydata was an object to avoid prototypical inheritance issues. When evaluating an attribute with @ you have to make sure you pass it inside {{}}.
Nice! #highFive
Excellent post, i didn’t understand on egghead but now m clear with it
thanks a ton..
Really good post, thanks a lot
In the example of isolated scope ‘&’, `updateFoo` isn’t mentioned anywhere. So it’s very puzzling, _exactly how_ angular knows to call it.
Probably just another thing that slipped through the cracks.
Not so my friend 😀 In the example, when you declare the directive, updateFoo is referenced in the markup via isolated-expression-foo=”updateFoo(newFoo)”. When isolatedExpressionFoo is fired in the directive, it actually delegates control to whatever method you declare on the directive which in this case is udpateFoo.
Crisp and Clear. Kudos !
this example cannot work in angularJS 1.5.x ?
It could with a few minor tweaks. 😀
“If both my isolated property and parent property were attributeFoo, then I would simply have to do this:”
do you mean to say the parent controller will somewhere have a scope property of attributeFoo? Or it should be If the isolated scope property and the attribute name is same
I don’t see that in the example
app.controller(“mainCtrl”, function($scope){
$scope.attributeFoo = “some text”;
});
Hi,
http://jsfiddle.net/simpulton/VJ94U/
I am trying this demo in my system for same code close of sticky note not working in this ,
instead of below
ondelete not working
i changed
because , for ondelete in my case delete event is not generating.
second in my case, id is not passing for deleteNote(id) on ng-click,
$scope.deleteNote = function (id) {
notesService.deleteNote(id);
};
Could provide me for this solution so that it will be helpful.
I understand the scopes in directives finally. Great article!
Nice post, Thanks for sharing your views…!
Thanks for clarifying the isolated scopes in detail. It is really confusing to understand from the official documentation.