I'm reading the directives section of the developers guide on angularjs.org to refresh my knowledge and gain some insights and I was trying to run one of the examples but the directive ng-hide is not working on a custom directive.
Here the jsfiddle: http://jsfiddle.net/D3Nsk/:
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
Does Not Work Here!!!
</my-dialog>
<div ng-hide="dialogIsHidden">
It works Here.
</div>
Any idea on why this is happening?
Solution
Seems that the variable dialogIsHidden on the tag already make a reference
to a scope variable inside the directive and not to the variable in the controller; given
that the directive has it's own insolated scope, to make this work it's necesary to pass
by reference the variable dialogIsHidden of the controller to the directive.
Here the jsfiddle:
http://jsfiddle.net/h7xvA/
changes at:
<my-dialog
ng-hide="dialogIsHidden"
on-close="hideDialog()" dialog-is-hidden='dialogIsHidden'>
and:
scope: {
'close': '&onClose',
'dialogIsHidden': '='
},
You're creating an isolated scope inside your directive when asigning an object to scope. This is why $scope.dialogIsHidden is not passed through to the directive and thus the element is not being hided.
Kain's suggested adjustment for the fiddle with using $parent illustrates this.
your can change the
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
to
<my-dialog ng-hide="$parent.dialogIsHidden" on-close="hideDialog()">
Related
I am facing a strange situation in which I have created a directive, to which a controller is attached, and one of the two tiny functions of the controller is never called from the view whereas the other function is.
Here is the plunker.
The message I expect is (bold is what does not show up)
You are limited to: Prison
I have already created tens of directives, whether in their own right or as wrappers around existing directives available on GitHub, from lightweight ones such as custom-select to behemoths such as angular-ui-grid.
I am at the end of my wits here as to why {{getArea()}} produces no text at all in the view. I've scrutinized the code, trying to do it with new eyes, so to speak, and I see nothing wrong. I've created a specific project in Eclipse for this tiny piece of code, installed Wampserver just so I could set breakpoints in Firebug and God knows to what great lengths I had to go just so that I could understand what is wrong with the code I wrote.
For instance, in isRestricted(), I can call getArea() without any problem. However, Angular seems to not find the function from the directive.
A few similar questions have already been asked but none of the errors (missing controller or ng-app specification, missing dependency list at module declaration, nested controllers, etc.) seem to apply. There's obviously an important lesson to be learned here and I'm truly eager to learn it.
EDIT: The lesson learned is that ng-if creates a new scope. That new scope comes in between the controller and the directive, which leads to the template of the directive losing access to anything defined in the controller (at least, that's how I would phrase it). (Note that a comment hinted at directive priority.)
There are several solutions, which all maintain the prototypical inheritance needed for the template to access the functions defined in the controller:
not using an isolate scope
not defining the ng-if directive on the top-level element of my directive, as that causes a conflict (between my controller's scope and the scope defined by ng-if). I believe ng-if wins here, which leads to the controller's scope being out of reach of the directive. Using ng-if on a child div does the trick (because then, the ng-if scope inherits my controller's scope, hence making the functions available to the template).
Because of the CSS styling needed with this directive, I have used scope: false.
<span class="scoop-badge-content">{{$parent.getArea()}}</span>
Or in directive :
scope:true
This is because ng-if use how own scope
The strange thing is that when i have this problem i usually use dot notation. But it doesn't work here, probably because we're inside a directive, and i didn't had the case until now.
EDIT : a last way of doing this chaging the template :
<div class="scoop-badge scoop-badge-ua">
<div ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I think this work because you have replace true and ng-if will conflict with ng-scope if it's on the top DOM element.
When you have scope = {} in your directive, Angular creates an isolated scope. Which means it can't get to the getArea() function.
You can completely remove the scope = {} line or set it to scope = true or scope = false depending on what you're trying to achieve later on.
When set to scope = true Angular will create a new scope object and assign it to the directive. This scope object is prototypically inherited from its parent scope.
When set to scope = false the directive will use its parent scope. (This is the default value. It has the same effect if you remove this line).
More information about scopes here
Removing scope: {} from directive definition solves problem.
app.directive('scoopBadgeUa', function() {
return {
restrict : "A",
scope: {}, // This is not needed, creates conflict
templateUrl : "scoop-badge-ua.html",
replace : true,
controller : 'ScoopBadgeUaController',
};
});
Your code is correct, you don't have to do anything more than adding a <div></div> wrapping your code in scoop-badge-ua.html.
<div>
<div class="scoop-badge scoop-badge-ua" ng-class="{'visible': isRestricted()}" ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I like this solution better than transclude: true because it doesn't include the directive's tag in your html code, which ultimately translates to a cleaner code.
I am trying to make a transitive transclusion, or call it “directive inception”.
I made this example to illustrate what I am trying to do:
http://plnkr.co/edit/0hFFHknDps2krtK1D9ud?p=preview
The directive “first” wraps the directive “second” in its template and the two of them use transclusion.
What I want to do is to bind a value from a controller to the html that is a child of the “first” directive.
So I wanted my example to display:
<h1>Chained transclusions test</h1>
<div>
<h2>First directive</h2>
<div>
<h2>Second directive</h2>
<div>Controller hello</div>
</div>
</div>
Obviously that is not what I got.
I tried to analyze the scope with the developer tool and I was surprised by the result scope tree:
the result trees
I thought angularJS would create a new scope when using the transclude feature in a directive. And that this scope would be a non isolate sibling of my directive isolate scope. But I cannot see any sibling of my first directive scope (although it uses transclude). Plus, every children of my “first” directive has a scope isolated from the controller scope since the “first” directive scope is an isolated one.
I don’t understand the behavior here.
Is the transclude inclusion completely forbidden in angularJS ?
Is it possible to create a directive with transclusion, that wraps another directive that uses transclusion ?
It seems to me that this is the whole power behind web components, the fact that transclusion or any other special caracteristics should be seen as “implementation detail”, and the component should be able to use other directives that hide their own implementation details.
Without getting into the details of scope creation when using isolate scope with transclusion... it is possible to nest transclusions, but in your example, you need to make scope.controllerMsg available to the first directive's isolate scope:
JS:
app.directive('first', function(){
return {
...
scope: { controllerMsg: '=text'},
...
}
});
HTML:
<first text="controllerMsg">
{{controllerMsg}}
</first>
Demo
When I try to nest two directives on the same element I get the following error.
Nested "E" directives - Multiple directives [...] asking for new/isolated scope,
I want to nest two "E" isolated scoped directives, like in this fiddle.
(To actually reproduce the problem, you need to uncomment the <lw> directive)
I keep getting this error that I don't understand/know how to fix:
Error: [$compile:multidir] Multiple directives [lw, listie] asking for new/isolated scope on: <listie items="items items">
Wasn't this supposed to work?
Thanks!
remove replace: true on the directive by name 'lw' ..
That should also solve .
updated fiddle : updated fiddle
app.directive('lw', function(){
return {
restrict: 'E',
scope: {
items: "="
},
template: '<listie items="items"></listie>',
controller: function($scope) {
}
}
});
Analysis :
what caused the problem ?
with replace=true for the lw directive what happens is lw has isolate scope, now as replace=true , the replaced element which itself has isolate scope was tried to be replaced there, so what you unknowingly did is you tried to give two scopes for the same element listie.
code level observation in angular.js version 1.2.1:
line 5728 : function applyDirectivesToNode is the function that executes compile on directive and here in line 5772 they do this checking if we are trying to assign duplicate scope or in other words same element with two scopes .
function assertNoDuplicate(what, previousDirective, directive, element) {
if (previousDirective) {
throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}',
previousDirective.name, directive.name, what, startingTag(element));
}
}
above is the function which does that check and if in case there is an attempt to assign two scopes to same element it flings that error .
So this is how it is designed to be and not a bug .
why replace:true removal solves the problem ?
by removing replace:true what happened is instead of new directive listie replaced instead of lw , it got appended into it , so it is one isolated scope nested into other, which is absolutely correct and allowed .
the nested isolate scope is like
<lw items="items" class="ng-isolate-scope">
<div items="items" class="ng-isolate-scope">
..........
</div>
</lw>
why wrapping in a div will also work (this is your solution which you considered to be workaround) ?
The point to be noted is that the div is not an element with a separate isolate scope .It is just an element.
here on replace you are attaching the isolate scope of lw to be attached to a div . (NOTE : The div by itself does not have an isolate scope ), so there is no 2 isolate scopes attached
to same element div .
so there is no duplication so the assert step passed and it started working .
So this is how it is designed to work and definitely its not a bug .
Just for interest sake, I had the same error. It turned out because I did the old "Cut and Paste" to create a new directive and they had the same name to start with. I forgot to change it which produced this error.
So for anyone who is as bad as me then here's a possible solution to check.
I managed to fix it by replacing the template code in the wrapper directive.
Here's the update fiddle.
This is what changed.
template: '<div><listie items="items"></listie></div>',
//template: '<listie items="items"></listie>', bug?
This solves my problem, but it looks like a bug to me. I guess I'll create an issue on Github.
There - https://github.com/angular/angular.js/issues/5428
My problem was because I was including the directive JS file twice
I was using Avengers example and after updating to angular 1.4 I started getting this. As it turned out, it had a controller and a directive fighting for scope on the same line
<div my-directive ng-controller="myController as vm"></div>
So moved the controller to a separate scope to fix it.
<div my-directive >
<div ng-controller="myController as vm">
</div>
</div>
The accepted response here solved the problem for me.
Essentially remove the isolated scope from the directive and instead pass the scope property via the directive's link function (in the attributes parameter).
In my case I had a bootstrap-ui accordion-group directive on an element that had ng-repeat:
<accordion-group is-open="status.open" ng-repeat="receipt in receipts">
And solved it by doing this:
<div ng-repeat="receipt in receipts">
<accordion-group is-open="status.open">
I have a directive that I use like this:
<dir model="data"></dir>
The directive has an isolated scope.
scope :{
model:'='
}
Now I'm trying to use ng-show on that directive using another attribute of my page's $scope, like this:
<dir ng-show="show" model="data"></dir>
But it's not working because the directive is trying to find the show attribute on its own scope.
I don't want the directive to know about the fact that its container might choose to hide it.
The workaround I found is to wrap the directive in a <div> and apply ng-show on that element, but I don't like the extra element this forces me to use:
<div ng-show="show" >
<dir model="data"></dir>
</div>
Is there a better way of doing this?
See this plunker: http://plnkr.co/edit/Q3MkWfl5mHssUeh3zXiR?p=preview
Update: This answer applies to Angular releases prior to 1.2. See #lex82's answer for Angular 1.2.
Because your dir directive creates an isolate scope, all directives defined on the same element (dir in this case) will use that isolate scope. This is why ng-show looks for property show on the isolate scope, rather than on the parent scope.
If your dir directive is truly a standalone/self-contained/reusable component, and therefore it should use an isolate scope, your wrapping solution is probably best (better than using $parent, IMO) because such directives should normally not be used with other directives on the same element (or you get exactly this kind of problem).
If your directive doesn't need an isolate scope, your problem goes away.
You could consider migrating to Angular 1.2 or higher. The isolate scope is now only exposed to directives with a scope property. This means the ng-show is not influenced by your directive anymore and you can write it exactly like you tried to do it in the first place:
<dir ng-show="show" model="data"></dir>
#Angular-Developers: Great work, guys!
Adding the following into the link function solves the problem. It's an extra step for the component creator, but makes the component more intuitive.
function link($scope, $element, attr, ctrl) {
if (attr.hasOwnProperty("ngShow")) {
function ngShow() {
if ($scope.ngShow === true) {
$element.show();
}
else if($scope.ngShow === false) {
$element.hide();
}
}
$scope.$watch("ngShow", ngShow);
setTimeout(ngShow, 0);
}
//... more in the link function
}
You'll also need to setup scope bindings for ngShow
scope: {
ngShow: "="
}
Simply use $parent for the parent scope like this:
<dir ng-show="$parent.show" model="data"></dir>
Disclaimer
I think that this is the precise answer to your question but I admit that it is not perfect from an aesthetical point of view. However, wrapping the <div> isn't very nice either. I think one can justify it because from the other parameter passed to the isolate scope, it can be seen that the directive actually has an isolate scope. On the other hand, I have to acknowledge that i regularly forget the $parent in the first place and then wonder why it is not working.
It would certainly be clearer to add an additional attribute is-visible="expression" and insert the ng-show internally. But you stated in your question that you tried to avoid this solution.
Update: Won't work in Angular 1.2 or higher.
I'm new to Angular JS. In my plunkr, I have a typeahead that works when I have the typeahead in the html markup. However, when I dynamically generate the html inside my directive, the typeahead no longer works.
Code Here:
http://plnkr.co/edit/KdrxptYAnpTmKa7ZuKkM?p=preview
and to take it one step further, when I pass in a function, it still does not work:
http://plnkr.co/edit/jqN913hJxuVSFAZxAQt7?p=preview
It is not a trivial problem you are trying to solve here, I'm afraid. Basically you are bumping into the scoping issue. The typeahead directive evaluates it expression (city for city in cities($viewValue) here) in the scope of the DOM element on which it is placed. The way you written your wrapper directive makes it so the expression is evaluated in the directive's scope which is isolated and doesn't "see" your controllers scope.
The are number of ways around it but probably the simplest one is to link your $compiled-ed element in the scope that is $parent of your directive scope:
var linkedInput = $compile(inputHtml)(scope.$parent);
Here is a working plunk: http://plnkr.co/edit/fLFwIKNqIRbnesMjZBGj?p=preview
The other alternative is to loose an isolated scope and "manually" take care of the 2-way data binding with the help from the $parse service.