I have two simple directives who generate similar elements in the DOM.
Both have an isolated scope
Both have an ng-click to a metod that displays a message.
One has the ng-click declared in the html file and that one triggers the method in the containing scope.
The other has the ng-click in the directive template and a click triggers the method in the isolated scope
Why doesn't both trigger the method in the isolated scope?
Here is a Plunk
Javascript
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.alertMessage = function(){
alert('I live in mainController');
}
});
app.directive('isolated', function(){
return {
scope: {},
link: function(scope, element, attrs) {
scope.alertMessage = function(){
alert('I live in the isolated directive');
};
}
};
});
app.directive('isolatedWithTemplate', function(){
return {
scope: {},
replace: true,
template: '<button ng-click="alertMessage()">Press me</button>',
link: function(scope, element, attrs) {
scope.alertMessage = function(){
alert('I live in the isolated directive');
};
}
};
});
Html
<button isolated ng-click="alertMessage()">Press me</button>
<div isolated-with-template></div>
(Answer updated with corrections more info as pointed out by #Edminsson)
Refer to following link to understand the scopes - although the link does not explain the above behavior my answer will try to do explain.
Understanding Scopes
Following will try to explain why the element with 'isolated' directive says 'I am in mainController scope' instead of 'I am in isolated scope.
Note the order of the two DOM element does not matter, neither the priority affects anything with respect to what we saw.
<div isolated-with-template></div>
<button isolated ng-click="alertMessage()">No Template</button>
Each directive on the element is compiled on the scope of the parent.
That means the ng-click got bound to the scope function in the MainController. The isolate directive does create a new isolated scope and the function on it but the ng-click is already bound to the function object $scope.alertMessage() from mainController.
Element with isolated-with-template is also getting compiled with the parent scope. However when it encounters the directive it has now a template. This gets compiled (to be precise nodeLinkFn. There is afterTemplateNodeLinkFn so directives with templates or templateUrls will use this). At this time the isolated scope has the function alertMessage from the isolated scope. Also know that the MainController's alertMessage was already defined on that scope prior to all this.
1) Angular processes the DOM depth first and links backwards
2) root of the template gets its immediate scope
3) when multiple directives are requesting scopes on an element you get only one scope
What happens with scope = true ?
In this plnkr you will notice that the directive no longer says 'I am in mainController'.
scope:true,
You are actually getting brand new child scope. When a linking function is created angular knows about this scope being a brand new scope.
One trick for isolated scopes
Try assigning template: ' ', and replace: true
and you have your isolated scope in action.
Isolated scope trick using another example
.directive("client", function() {
return {
restrict: "A",
template: ' ',//notice extra spaces
replace: true,//notice this
scope: {
name: "#name",
client: "=client"
}
};
});
<button isolated ng-click="alertMessage()">Press me</button>
in this case the ng-click is independent of isolated directive, it will work even if you change html to
<button ng-click="alertMessage()">Press me</button>
so the ng-click above is not working on isolate directive but on scope of your controller...
the ng-click of isolated-with-template is working in scope of a directive..
Related
I need to do this to make use of the <dialog> tag in HTML5, I want every <dialog> on my site to have its own unique scope accessible using the controller and controllerAs syntax.
Here is what I thought would work.
Javascript
\\Dialog Controller
function dialog () {
return {
scope: {},
controller: function () {
this.test = 'Dialog Test';
},
controllerAs: 'Dialog',
bindToController: true
}
}
angular.module('app',[]).directive('dialog', dialog);
HTML
<!-- Dialog HTML Example Case -->
<body ng-app='app'>
<dialog id='test'>{{Dialog.test}}</dialog>
</body>
I would expect that when the dialog was activated Dialog.test would evaluate to Dialog Test. What happens instead is that it evaluates to an empty string. What's more is that if I add a controller to the body, the dialog has access to its scope. It is as though the isolate scope definition in my directive is completely ignored.
Plunk
Note that I have modified the plunk to use <span> instead of <dialog> due to the lack of support in most browsers.
http://plnkr.co/edit/eXtUq7BxCajOZAp8BpVe?p=preview
You are creating isolated scope, thats good thing. But after AngularJS1.2 version, they have done some breaking changes, where isolated scope will be completely isolated.
So Span directive's scope will be visible to template of that directive(Span) only.
And inner html of that directive will get only parent/current scope only instead of directive isolated scope(As Isolated Scope will be visible to template only). To print value of Span.test, you have to create template and refer that template in your directive as below:
var app = angular.module('test', []);
function mainCtrl() {
this.test = 'test';
};
function spanCtrl() {
this.test = 'Span Test';
}
function span () {
return {
scope: {},
controller: spanCtrl,
controllerAs: 'Span',
template: '{{Span.test}}'
}
}
app.controller('mainCtrl', mainCtrl);
app.directive('span', span);
You can checkout two awesome blog for more detail Component In AngularJS and Transclude In AngularJS
Contents of an element do not "see" the isolate scope of a directive. The fact that the scope is "isolate" means that it is separate from the scope of the View where both the directive and contents of its hosting element reside.
To link the contents against the internal scope, you'd need to transclude them - the transclude function allows you to link it against any scope:
function dialog () {
return {
scope: {},
controller: function () {
this.test = 'Dialog Test';
},
controllerAs: 'Dialog',
bindToController: true
transclude: true.
link: function(scope, element, attrs, ctrls, transclude){
// scope here is the isolate scope of the directive
transclude(scope, function cloneAttachFn(contentsClone){
element.append(contentsClone);
});
}
}
}
The above would work, but it's still important to understand why the "default" behavior makes sense. Consider that to a user of your directive, the directive's internal (i.e. isolate) functionality (and thus scope variables) ought to be invisible. If it wasn't, the user of your directive would need to know about "special" scope variables, like Dialog, in your case. So, someone who reads your HTML would have no idea where Dialog came from without knowing how the directive operates.
There is a mistake in original assertion: the contents of dialog directive node
<dialog id='test'>{{Dialog.test}}</dialog>
belong to parent's scope, not to directive's scope. Therefore, it interpolates Dialog.test from parent controller (or root scope if there is none).
It is possible to achieve the behaviour that was expected with something like this:
app.directive('dialog', function ($interpolate) {
return {
scope: {},
controller: function ($scope) {
this.test = 'test';
},
controllerAs: 'Dialog',
compile: function (element) {
var template = element.text();
return function (scope, element) {
element.text($interpolate(template)(scope));
}
}
};
});
But it can hardly be called a promoted way to use Angular. Let the directive handle its template.
I have been working with isolated scope directives for a little time and a question came in mind watching it's behavior:
Why can't i bind variables that i define inside the directive inherited scope directly to the view?
Let me show an example on this code pen:
http://codepen.io/anon/pen/VLKjrv
When i create a new $scope variable inside the directive controller and i try to bind it on the view, it does not works.
By the other hand, when i bind that variable on a html that comes from the template directive attribute, it does works.
Check out the code:
<body ng-app="isolated-test-app">
<section ng-controller="isolatedTestCtrl">
<article>
<h1>test1</h1>
<div isolated-directive binding-from-attr="test">
<span ng-bind="test"></span>
<span ng-bind="test2"></span>
</div>
<h1>test2</h1>
<div isolated-directive-number-two binding-from-attr="test">
</div>
</article>
</section>
angular.module('isolated-test-app', [])
.controller('isolatedTestCtrl', function isolatedTestCtrl($scope){
$scope.test = 'Binded from parent controller';
})
.directive('isolatedDirective', function isolatedDirective(){
var directive = {
scope: {
bindingFromAttr: '=',
},
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
.directive('isolatedDirectiveNumberTwo', function isolatedDirective2(){
var directive = {
scope: {
bindingFromAttr: '=',
},
template:'<span ng-bind="bindingFromAttr"></span>\
<span ng-bind="test2"></span>',
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
test1
Binded from parent controller
test2
Binded from parent controller
Binded from directive controller!
I was expecting the result of test2 on test1.
Why does that happens?
There is a difference between directive template and the directive's element's contents with regards to what scope applies.
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent. Also, note, that the contents would be replaced by the template, if the template is defined. To use the contents in addition to the template requires "transcluding" (transclude: true) (this is, however, outside of scope for this answer).
If you are confused, you could always check $scope.$id to see which scope applies:
<div>parent scope: {{$id}} (will be 2)</div>
<isolate-scope>
contents of isolate scope directive: {{$id}} (will also be 2)
</isolate-scope>
<isolate-scope-with-template>
contents will be replaced with the template
</isolate-scope-with-template>
.directive("isolateScopeWithTemplate", function(){
return {
scope: {},
template: "template: {{$id}} (will NOT be 2)"
}
})
(of course, the actual $id could be different)
In child scope (scope: true) directives, the scope that applies to the content is actually the same that would have applied to the template (same here - the template would replace the contents if it exists, unless you transclude).
Now, to answer your question:
The first <span ng-bind="test2"></span> binds to a non-existent $scope.test2 in the parent scope and so it is empty.
But the <span ng-bind="test2"></span> in the template of isolatedDirectiveNumberTwo binds to the isolate scope of that directive, which defines $scope.test2 = 'Binded from directive controller!'.
This is my guess base on experiment in http://codepen.io/anon/pen/MwjjBw
so for test 1, the directive scope doesnt have test/test2 since the dom object is belong to controller. Hence in order to update it you have to use
$scope.$parent.test2 = "" ;
and for test 2, as the template is created as part of directive hence the dom object is belong to directive and also accessible by controller ( I guess $compile adding this into controller scope/watch).
You can also see that test1 doesnt have any watcher as there is no binding happen.
Now i get the whole picture, as New Dev's answer stated on his answer
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent.
So i have learned that there is a difference between directive contents and template and how the scope is inherited on isolated scopes.
For my application setting the scope to true solved entirely my problem.
Also, kwan245's solution is a real good work-around of this issue.
Both answers cleared my mind, many thanks to New Dev and kwan245 :)
I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ
I am trying to build generic code as much as possible.
So I'm having 2 directives, one nested inside the other while I want the nested directive to call a method on the main controller $scope.
But instead it requests the method on the parent directive, I want to know how to execute a method against the main controller scope instead of the parent directive.
Here is a sample code for my issue
My HTML should look something like this:
<div ng-controller='mainctrl'>
<div validator>
<div datepicker select-event='datepickerSelected()'/>
</div>
</div>
Javascript:
var app = angular.module("app",[]);
var mainctrl = function($scope){
$scope.datepickerSelected = function(){
//I WANT TO ACCESS THIS METHOD
}
}
app.directive("validator",function(){
return {
scope : {
//the datepicker directive requests a datepickerSelected() method on this scope
//while I want it to access the mainctrl scope
}
link: function(scope){
//some code
}
}
});
app.directive("datepicker", function(){
return{
scope: {
selectEvent: '&'
}
link: function(scope, elem){
//example code
$(elem).click(scope.selectEvent); //want this to access mainctrl instead validator directive
}
}
});
Simply remove the validator directive's scope property, thus eliminating its isolated scope. That means that validator will have the same scope that it is nested in (your controller) and datepicker will use that scope.
Another option if you want both to have isolated scopes (doesn't sound like you do) is to pass the function through to "validator's" scope.
I have a directive which looks something like:
var myApp = angular.module('myApp',[])
.directive("test", function() {
return {
template: '<button ng-click="setValue()">Set value</button>',
require: 'ngModel',
link: function(scope, iElement, iAttrs, ngModel) {
scope.setValue = function(){
ngModel.$setViewValue(iAttrs.setTo);
}
}
};
});
The problem is that if I use this directive multiple times in a page then setValue only gets called on the last declared directive. The obvious solution is to isolate the scope using scope: {} but then the ngModel isn't accessible outside the directive.
Here is a JSFiddle of my code: http://jsfiddle.net/kMybm/3/
For this scenario ngModel probably isn't the right solution. That's mostly for binding values to forms to doing things like marking them dirty and validation...
Here you could just use a two way binding from an isolated scope, like so:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
target: '=target',
setTo: '#setTo'
},
template: '<button ng-click="setValue()">Set value</button>',
controller: function($scope) {
$scope.setValue = function() {
$scope.target = $scope.setTo;
};
//HACK: to get rid of strange behavior mentioned in comments
$scope.$watch('target',function(){});
}
};
});
All you need to do is add scope: true to your directive hash. That makes a new inheriting child scope for each instance of your directive, instead of continually overwriting "setValue" on whatever scope is already in play.
And you're right about isolate scope. My advice to newbies is just don't use it ever.
Response to comment:
I understand the question better now. When you set a value via an expression, it sets it in the most immediate scope. So what people typically do with Angular is they read and mutate values instead of overwriting values. This entails containing things in some structure like an Object or Array.
See updated fiddle:
http://jsfiddle.net/kMybm/20/
("foo" would normally go in a controller hooked up via ngController.)
Another option, if you really want to do it "scopeless", is to not use ng-click and just handle click yourself.
http://jsfiddle.net/WnU6z/8/