Variables in directive isolate scope not available - angularjs

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.

Related

Custom range angular directive with events

I'd like to create an range directive to repeat the included html x times.
Like this:
...
.directive('raichuRange', function () {
return{
scope:{
range: '=raichuRange'
},
transclude: 'element',
link : function(scope, element, attrs, ctrl, transcludeFn){
var parent = element.parent();
for(var i = 1;i<= scope.range;++i){
var childScope = scope.$new();
childScope.$index = i;
transcludeFn(childScope, function (clone) {
parent.append(clone);
})
}
}
}
})
The problem with this is: any ng-click directives within don't respond anymore.
Why doesn't this work? https://jsfiddle.net/pq1wkp90/
Because the directive uses an isolate scope, the parent functions are not inherited by the directive scope or their children.
To access the functions on the parent scope of the directive use:
ng-click="$parent.$parent.test($index)"
Use one $parent to access the isolate scope from the transcluded element. The second $parent to access the parent of the isolate scope.
For more information on directive scopes, AngularJS $compile API Reference -- scope.
PS. Also change $scope.$index = $index + 1; to $scope.$index = $scope.$index + 1;

Pass variable to AngularJS directive without isolated scope

I am learning AngularJS directive, and one thing I want to do is to pass some variable $scope.message in the parent scope (a scope of a controller), and I want it to be renamed to param inside the directive alert. I can do this with an isolated scope:
<div alert param="message"></div>
and define
.directive("alert", function(){
return{
restrict: "A",
scope: {
param: "="
},
link: function(scope){
console.log(scope.param) # log the message correctly
}
}
})
But can I do this without using isolated scope? Suppose I want to add another directive toast to the <div toast alert></div> and utilize the same param (keeping the 2-way data-binding), naively I will do
.directive("toast", function(){
return{
restrict: "A",
scope: {
param: "="
},
link: function(scope){
console.log(scope.param)
}
}
})
I surely will get an error Multiple directives [alert, toast] asking for new/isolated scope on:<div...
So in all, my question is, how to rename parent scope variable without isolated scope, and how to share variables when two directives are placed on a single DOM?
Modify your toast directive:
.directive("toast", function(){
return{
restrict: "A",
link: function(scope, elem, attrs){
var param = scope.$eval(attrs.param);
console.log(param)
}
}
})
Example fiddle.
Since toast is now on the same scope as the parent would have been (if it was allowed to be isolate scope), you can simply call $eval on scope with the param attribute to get the value.

Directive's isolated scope variables are undefined if it is wrapped in a directive which has in its template ng-if and tranclusion enabled

I am facing is an issue which is demonstrated in the following example:
http://jsfiddle.net/nanmark/xM92P/
<div ng-controller="DemoCtrl">
Hello, {{name}}!
<div my-wrapper-directive>
<div my-nested-directive nested-data="nameForNestedDirective"></div>
</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('DemoCtrl',['$scope', function($scope){
$scope.name = 'nadia';
$scope.nameForNestedDirective = 'eirini';
}])
.directive('myWrapperDirective', function(){
return {
restrict: 'A',
scope: {},
transclude: true,
template: "<div ng-if='isEnabled'>Hello, <div ng-transclude></div></div>",
link: function(scope, element){
scope.isEnabled = true;
}
}
})
.directive('myNestedDirective', function(){
return {
restrict: 'A',
scope: {
nestedData: '='
},
template: '<div>{{nestedData}}</div>',
};
});
I want to create a directive (myWrapperDirective) which will wrap many other directives such as 'myNestedDirective of my example.
'myWrapperDirective' should decide if its content will be displayed or not according to ng-if expression's value, but if contents is a directive like 'myNestedDirective' with an isolated scope then scope variable 'nestedData' of 'myNestedDirective' is undefined.
The problem is with the double-nested isolated scopes. You see, you are using the nameForNestedDirective variable, defined in the outer scope from the inner scope which is isolated. This means it does not inherit this variable, thus undefined is passed to the nested directive.
A diagram to explain:
Outer scope - DemoCtrl
- Defines: name
- Defines: nameForNestedDirective
+ Uses: name
Inner isolated scope 1 - myWrapperDirective
- Defines: (nothing)
- Inherits: (NOTHING! - It is isolated)
+ Uses: (nothing)
* Passes nestedData=nameForNestedDirective to nested directive, but
nameForNestedDirective is undefined here!
Inner isolated scope 2 - myNestedDirective
- Defines: nestedData (from scope definition)
- Inherits: (NOTHING! - It is isolated)
+ Uses nestedData
You can convince yourself this is the case by commenting out the scope definition of the wrapper directive ("hello eirini" is displayed as expected):
.directive('myWrapperDirective', function(){
return {
...
//scope: {},
...
I am not sure if the wrapper directive really needs to have an isolated scope. If it doesn't, maybe removing the isolated scope will solve your problem. Otherwise you will have either to:
Pass the data first to the wrapper and then to the nested directives
Pass the data to the wrapper directive, write a controller for it that exposes the data and then require the wrapper controller from the nested directive.

AngularJS directive transclude part binding

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.

dynamic directives in angularjs

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

Resources