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.
Related
I'm trying to write attribute directive that wraps element and decorates it with some other things along with additional directives. And I need to keep content inside the element intact. Something like this:
app.controller 'myCtrl', ($scope)->
$scope.name = "Gwendolyn"
app.directive 'niceName',($compile)->
restrict: 'A'
replace: yes
scope: niceName:"="
template: (element, attrs)->
"<div>
<div style='background:lightgray'>
#{element.html()}
</div>
<div>
<h1>{{nice}}</h1>
</div>
</div>"
controller: ($scope, $element)->
$scope.$watch 'hover',->
$scope.nice = if $scope.hover then $scope.niceName else ''
compile:(element, attrs)->
element.attr('ng-mouseover',"hover = true")
element.attr('ng-mouseout',"hover = false")
element.removeAttr('nice-name') # removing itself to avoid from falling into infinite loop
pre:(scope, iElement, iAttrs)->
$compile(iElement)(scope)
Markup:
<div nice-name="uglyname">
<h2>{{uglyname}}</h2>
<div>
Now that thing doesn't work at all, it won't render h2 part because now uglyname is unknown to the current scope. I can compile it with passing the parent scope, but then it totally breaks my controller. (see jsbin below)
So somehow I have to compile content of the element applying parent scope, add it to the template and then compile again applying local scope? Or I need to find a way to inherit properties of parent scope? Or I can't do it with attribute directive? Maybe I need to use element directive and transclusion?
Any ideas?
jsbin
i would do what #marfarma suggested as well as change you html to not include the brackets so:
scope: { niceName: "="},
and
nice-name="uglyname"
How can I access the directive's isolate scope in the directive's body? My DOM looks like this:
<div ng-app="app">
<directive>
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
The boolProperty is assigned inside the directive's link function:
angular.module("app", []).directive("directive", function() {
return {
scope: {},
link: function($scope) {
$scope.boolProperty = true;
}
};
});
The problem is, the child <p> inside the directive binds to the directive's parent scope, not the directive's isolated scope. How can I overcome this?
Click here for jsFiddle.
There are couple of problems in your code.
The default restrict option is A for attribute so anyways your directive will not be compiled because you are using it as an element. Use restrict: 'E' to make it work.
As per the documentation, the scope of the transcluded element is not a child scope of the directive but a sibling one. So boolProperty will always be undefined or empty. So you have to go up the scope level and find the proper sibling.
<div ng-app="app">
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty}}</p>
</directive>
</div>
and need to use transclusion in the directive as:
angular.module("app", []).directive("directive", function() {
return {
restrict: 'E',
scope: {},
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope) {
scope.boolProperty = true;
}
};
});
However, this approach is not advisable and break later If you add a new controller before the directive because transcluded scope becomes 2nd sibling unlike 1st as before.
<div ng-app="app">
<div ng-controller="OneCtrl"></div>
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty || $parent.$$childHead.$$nextSibling.boolProperty}}</p>
</directive>
</div>
Here is the Working Demo. The approach I mentioned is not ideal so use at your own risk. The #CodeHater' s answer is the one you should go with. I just wanted to explain why it did not work for you.
You forgot about two things:
By default AngularJS uses attrubute restriction, so in your case in directive definition you should specify restrict: "E"
You should use child scope, but not isolated. So set scope: true to inherit from parent view scope.
See updated fiddle http://jsfiddle.net/Y9g4q/1/.
Good luck.
From the docs:
As the name suggests, the isolate scope of the directive isolates everything except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.
It seems you would need to explicitly add boolProperty to scope.
<div ng-app="app" ng-controller="ctrl">
<directive bool="boolProperty">
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
JS
angular.module("app", []).controller("ctrl",function($scope){
$scope.boolProperty = false;
}).directive("directive", function() {
return {
restrict:"E",
scope: {boolProperty:'=bool'},
link: function($scope) {
$scope.boolProperty = "i'm a boolean property";
}
};
});
Here's updated fiddle.
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.
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