The directive's attributes don't change when the scope is updated, they still keep the initial value. What am I missing here?
HTML
<ul class="nav nav-pills nav-stacked" navlist>
<navelem href="#!/notworking/{{foo}}"></navelem>
<navelem href="#!/working">works great</navelem>
</ul>
<p>works: {{foo}}</p>
Javascript
(based on angular tabs example on front-page)
angular.module('myApp.directives', []).
directive('navlist', function() {
return {
scope: {},
controller: function ($scope) {
var panes = $scope.panes = [];
this.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
pane.selected = true;
}
this.addPane = function(pane) {
if (panes.length == 0)
this.select(pane);
panes.push(pane);
}
}
}
}).
directive('navelem', function() {
return {
require: '^navlist',
restrict: 'E',
replace: true,
transclude: true,
scope: { href: '#href' },
link: function(scope, element, attrs, tabsCtrl) {
tabsCtrl.addPane(scope);
scope.select = tabsCtrl.select;
},
template:
'<li ng-class="{active: selected}" ng-click="select(this)"><a href="{{href}}" ng-transclude></a></li>'
};
});
By defining scope: {} in your directive, it is creating a isolated scope.
So the parent scope is now invisible from the directive.
If you want to refer the parent scope, then you can put scope: true for shared
scope (among same directives) and omit the scope declaration for just normal scope nesting.
Or if you want to just refer $scope.foo of the parent, you can define
explicit scope variables like you've done in the child directive.
There are three types of directive scope inheritance:
No 'scope: ...' or explicit scope: false - no new scope is created. The directive uses the same scope as the parent. This is simple and convenient, but if you are building reusable components, this is not recommended, since the directive will likely only be usable if the parent scope has certain scope properties defined that the directive needs to use/access.
scope: true - creates a new scope, shared by all directives on the same element, with normal prototypical inheritance of the parent scope. Again, probably not the best choice for reusable components, since the directive probably shouldn't have access to the parent scope properties -- it could accidentally change something in the parent.
scope: { ... } - creates a new "isolated" scope -- it does not prototypically inherit from the parent scope. However, the object hash ( i.e., the { ... } ) allows us to define local directive scope properties that are derived from the parent scope -- so we can control which properties are shared, and how.
Use '=' for powerful 2-way binding between a parent scope property and a directive scope property -- changes to either scope property affect the other.
Use '#' for binding a parent's attribute value to a directive scope property. This is essentially 1-way binding. Only parent scope changes affect the directive scope.
Use '&' to bind to parent scope expressions/functions.
For your particular problem, you need to indicate in the object hash which scope properties you want to have 2-way binding.
For more about directive scopes (including pictures), please see section directives here: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
Like Mark Rajcok said - scope: {} will create a new isolated scope that don't inherit properties from parent, however we still can get access to these properties by using $parent property.
Controller:
app.controller('indexController', function($scope) {
$scope.test="Hello world!";
});
Directive
app.directive("test", function() {
return{
restrict: "A",
scope: {},
controller: function($scope){
console.log("directiv $scope.$parent.test: " + $scope.$parent.test);
console.log("directiv $scope.test: " + $scope.test);
}
};
});
output:
directiv $scope.$parent.test: Hello world!
directiv $scope.test: undefined
Related
I want to set default value in my directive like this but its going to be null
(function(angular) {
'use strict';
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {
name: '=?'
},
templateUrl: 'my-dialog.html',
link: function (scope, element) {
},
controller: function($scope) {
$scope.name = 'Tobias';
}
};
});
})(window.angular);
http://plnkr.co/edit/gVLvFwdXp0oxmJUOAMMN
Check demo: http://plnkr.co/edit/Ui9EqKNeUc3K01L1el0d?p=preview
Because when you call the directive, you do not pass in the binding property.
Bind the name to the directive:
<my-dialog name="name">Check out the contents, {{name}}!</my-dialog>
Since you are using transclude: true. Angular will create a separate scope for the (transclude scope) transcluded content, i.e., Check out the contents, {{name}}!. This scope is prototypically inherits from the parent scope, and it is a sibling of the isolated scope. So here {{name}} is not accessing the name on the isolate scope. Instead, it is accessing transclude scope (and traverse upstream to its parent scope, where you do not set the name).
Use <my-dialog name="name">... will create a name property in the parent scope and two-way bind to the directive. In this way, changing the value inside the directive will also change the value outside.
Colliding Scopes
You are having an issue because you have a transcluded scope and an isolate scope colliding. Remove scope: { name: '=' } to interpolate the name on the controller scope. Unless you need to keep the scope from inheriting from or colliding with the parent scope, an isolate scope isn't typically needed.
If you need to keep the directive's scope isolated then don't remove the above and add the name attribute to the element and set it to the scope property you want to use: name="name" for example.
Given the following:
scope: {
name: '='
}
This means you are two-way binding to the scope property defined in the name attribute on the element and calling it name on the isolate scope. If you said somename: '=thatName' then you would two-bind to the that-name attribute but call it "somename" on the isolate scope.
In two-way binding, changes on the directive scope will affect the parent scope. If you want to one-way bind (changes won't affect the parent) you can use name: '#' and if you want to bind to the value of the name attribute you can use name: '&'.
How do I make controller functions visible to a directive? Do I attach the methods to the scope, and inject the scope into the directive? Is it a good idea in the first place? I want to manipulate model data from within the UI.
It really dependes on what you want to do.
Since you want to access the controller's scope from the directive, I suggest you declare your directive with it's scope shared with the parent controller by setting it's scope prop to false:
app.directive('directiveName', function() {
scope: false,
link: function(scope) {
// access foo from the controler's scope
scope.foo;
}
});
This is a nice example with how directives can be hooked up to a controller
http://jsfiddle.net/simpulton/GeAAB/
DIRECTIVE
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
HOOKUP IN CONTROLLER
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
VIEW
<my-component ng-model="message"></my-component>
Adding to #grion_13 answer, scope:true would also work since it creates a new scope that is child of the parent scope so has access to parent scope data.
But a true reusable directive is one which get it input data using isolated scope. This way as long as your html+ controller can provide the right arguments to the directive isolated scope, you can use the directive in any view.
I'm experiencing some strange results in my code. For example, when you comment out the isolate scope line (scope: {local:'='}), the directive's scope has a local value, yet when you link the two, the local value is clearly sent to the parent scope, yet its deleted in the directive scope?
http://plnkr.co/edit/uBkCmnRQoj4B539nAkeW?p=preview
angular.module('My', [])
.controller('ctrl', function($scope){
$scope.parent = 2;
})
.directive('directive', function(){
return {
scope: { local: "=" }, // comment out this line... wtf?
link: function(scope, el, attr, ctrl){
scope.local = 88;
scope.doIt = function(){
scope.local = 77;
};
}
};
});
and
<body ng-app="My">
<div class="ctrl" ng-controller="ctrl">
<h3>ctrl</h3>
parent: {{parent}}
<div directive local="parent" class="directive">
<h3>directive</h3>
<div>local: {{local}}</div>
<button ng-click="doIt()">click</button>
</div>
</div>
</body>
Also, the ng-click="doIt()" doesn't seem to fire at all.
The problem is not of scope but of how your using the scope variables in your HTML.
By default, the directive's scope is not available in the HTML nested inside of it. You could use transclusion but often times a template is used for what you're doing in the Plunker:
.directive('myDirective', function(){
return {
scope: { local: "=" }, // comment out this line... wtf?
link: function(scope, el, attr, ctrl){
scope.local = 88;
scope.doIt = function(){
scope.local = 77;
};
},
template: '<h3>directive</h3><div>local: {{local}}</div><button ng-click="doIt()">click</button>'
}
})
Forked Plunker
You have two cases here.
When you uncomment the scope portion you get two scopes in your application. One scope is controller scope (ctrl), and second one is scope of div which uses your custom directive. They are completely separate and share no data inherently. Custom directive scope does take value from parent scope, because you set it explicitly (scope: { local: "=" }, local="parent"). When you set local to new value in child, isolated scope, it will also be copied back to original field (parent) because you specified so (local: "="). If you only want local to be set, then tell Angular not to copy value from isolated scope back to parent scope (do this by using # instead of =; scope: { local: "#" })
ngClick directive also doesn't work, because as we said, your custom directive scope is isolated. When you add doIt there, it's not available in controller scope.
When you comment scope: { local: "=" } portion, now your controller and custom directive share same scope. Now doIt function is available.
I'd like to use a directive, transclude content, and call directive's controller method within the transcluded part:
<mydirective>
<div ng-click='foo()'>
click me
</div>
</mydirective>
app.directive "mydirective", ->
return {
restrict: 'EACM',
transclude: true
template: "<div ng-transclude></div>"
scope: { } #required: I use two way binding on some variable, but it's not the question here
controller: [ '$scope', ($scope)->
$scope.foo = -> console.log('foo')
]
}
plunkr here.
How can I do that please?
I have a different answer, which is not a hack and I hope it will be accepted..
see my plunkr for a live demo
Here is my usage of the directive
<div custom-directive custom-name="{{name}}">
if transclude works fine you should see my name right here.. [{{customName}}]
</div>
Note I am using customName within the directive and I assign it a value as part of the directive's scope.
Here is my directive definition
angular.module('guy').directive('customDirective', function($compile, $timeout){
return {
template : '<div class="custom-template">This is custom template with [{{customName}}]. below should be appended content with binding to isolated scope using the transclude function.. wait 2 seconds to see that binding works</div>',
restrict: 'AC',
transclude: true,
scope : {
customName : '#'
},
link : function postLink( scope, element, attrs, dummy, transcludeFn ){
transcludeFn( scope, function(clone, innerScope ){
var compiled = $compile(clone)(scope);
element.append(compiled);
});
$timeout( function(){
scope.customName = 'this stuff works!!!';
}, 2000);
}
}
});
Note that I am changing the value on the scope after 2 seconds so it shows the binding works.
After reading a lot online, I understood the following:
the ng-transclude directive is the default implementation to transclusion which can be redefined per use-case by the user
redefining a transclusion means angular will use your definition on each $digest
by default - the transclusion creates a new scope which is not a child of the isolated scope, but rather a sibling (and so the hack works). If you redefine the transclusion process you can choose which scope is used while compiling the transcluded content.. -- even though a new scope is STILL created it seems
There is not enough documentation to the transclude function. I didn't even find it in the documentation. I found it in another SO answer
This is a bit tricky. The transcluded scope is not the child of the directive scope, instead they are siblings. So in order to access foo from the ng-click of the transcluded element, you have to assign foo to the correct scope, i.e. the sibling of the directive scope. Be sure to access the transcluded scope from the link function because it hasn't been created in controller function.
Demo link
var app = angular.module('plunker', []);
app.directive("mydirective", function(){
return {
transclude: true,
restrict: 'EACM',
template: "<div> {{ name }} <br/><br/> <div ng-transclude> </div></div>",
scope: { },
link: function($scope){
$scope.name = 'Should change if click below works';
$scope.$$nextSibling.foo = function(){
console.log('foo');
$scope.name = 'it works!';
}
}
}
})
Another way is assigning foo to the parent scope because both prototypally inherits from the parent scope, i.e.
$scope.$parent.foo = ...
Technically, if you remove scope: { }, then it should work since the directive will not create an isolated scope. (Btw, you need to add restrict: "E", since you use the directive as element)
I think it makes more sense to call actions defined in parent scope from directive rather than call the actions in the directive from parent scope. Directive should be something self-contained and reusable. The actions in the directive should not be accessible from outside.
If you really want to do it, you can try to emit an event by calling $scope.$broadcast(), and add a listener in the directive. Hope it helps.
I am writing my angularjs directive with definition like:
return {
restrict: 'EA',
link: link,
scope: true,
transclude: true,
replace: true,
controller: controller,
template: '<div class="wizard">' +
'<div ng-transclude></div>' +
'</div>'
};
I notice two scopes was created:
< Scope (003) --- parent scope of directive
< Scope (004) --- controller scope of directive which I think is child scope created by 'scope=true'. all my functions, properites defined in controller show up in this scope
< Scope (005) --- transclude scope which is empty
from the document I am expecting only one child scope was created because 'scope=true' will not create an isolated scope. this leads all elements replaced by 'ng-transclude' actually inherit Scope(005) and have no access to my functions/properties defined in controller because they are in Scope(004) which is a sibling of Scope(005).
I don't know what's going wrong, can somebody throw some lights here?
And when using Chrome debugger to watch my elements, I notice these elements were added by a class "ng-scope", however, how can I match "ng-scope" to scopes showing in batarang console? like show ng-scope's id.
thanks
scope: true will create a new child scope that prototypically inherits from the controller scope – this is Scope 004.
scope: { ... } would create a new child scope that does not prototypically inherit from the controller scope.
Either way, a new child scope is created.
In addition, because you are using transclude: true, another (transcluded) child scope 005 is created. Transcluded scopes always prototypically inherit from the controller scope.
As you already discovered, properties and functions that you define on the directive scope (i.e., inside your directive) are not available to the view because the view uses the transcluded scope.
The picture above is based on the following code:
app.directive('myDirective', function() {
return {
restrict: 'EA',
//link: link,
scope: true,
transclude: true,
replace: true,
controller: function($scope) {
$scope.dirProp1 = "dirProp1";
$scope.dirFunc = function() {}
},
template: '<div class="wizard">' +
'<div ng-transclude></div>' +
'</div>'
};
});
function MyCtrl($scope) {
$scope.parentCtrlProp1 = 'ParentCtrlProp1';
}
So, as you can see from the diagram, the transcluded scope (hence the transcluded content) can only access properties and functions defined on the controller scope (003), via the prototype chain.
how can I match "ng-scope" to scopes showing in batarang console? like show ng-scope's
id.
I'm not aware of any way to do this (which is why I wrote a tool to draw my own pictures).
it's hard to figure out without any jsfiddle for the context. What is your link function? and what is your controller function?
By the way, it is the normal behaviour as documentation says transclude:true creates also a new scope.
Look at here : https://github.com/angular/angular.js/wiki/Understanding-Scopes
The transcluded and the isolated scope (if any) are siblings -- the
$parent property of each scope references the same parent scope. When
a transcluded and an isolate scope both exist, isolate scope property
$$nextSibling will reference the transcluded scope.
So if you want to access the 2 siblings from one another, you have to use a 2-way binding with their parent