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
Please help me understand scopes in AngularJS.
If I associate a controller within a directive (as opposed to within html), is it supposed to have any impact on the scope associated with the directive ?
How can I use ng-repeat after scope isolation ?
For e.g. here is an example: http://plnkr.co/edit/0flo5mru61r9h3H8kiW5?p=preview
ex1. If I comment out (div ng-controller="Ctrl")[line 40, 43] and instead uncomment (// controller: 'Ctrl')[line 35] within the directive, why aren't the same scopes/hierarchy created (as viewed in Batarang).
ex2. How can I run ng-repeat for instructorList and profList (separately) without changing the current controller and only playing around with the scope ?
I am not sure how to inspect plunkers in batarang, but.
If you do this, you're instantiating the controller twice: once on each directive element. Each time you instantiate it, you're creating a new scope. As such, you have two separate sibling scopes. You can see just from looking at the html that the heirarchy won't be the same as if you had them both within the same element that has its own scope. In the latter case, changes made by child element 1 would affect the same scope used by child element two.
It's not very clear what you mean here. ng-repeat should be done in html. You could put it in the template like this:
template: '<label ng-repeat="person in teacherList">{{person.id}}<input ng-model="person.name"><br></label>'
See this
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.
I want to write 'edit in place' directive in angularjs.
I want that directive is reusable, therefore I have following requirements on the directive:
it must be an attirbute that can deocorate any element, that makes sense (div,span,li)
it must support edit button, clicking on that will change set ot displayd elements into input fileds. Typically properties of one object, e.g. contact (number, name)
I disocvere trickery behaviour of scope visibility in the directive that can be seen in this fiddle http://jsfiddle.net/honzajde/ZgNbU/1/.
Comenting out in the directive: template and scope -> contact.number and contact.name are displayed
Comenting out in the directive: scope -> contact.number only is displayed
Not commenting out anything -> nothing is displayed
=> when both are commented out just adding template to the directive makes it render contact.number even though template is not used.
I am asking what are the rules of the game?
<div>
<div ng-controller="ContactsCtrl">
<h2>Contacts</h2>
<br />
<ul>
<li ng-repeat="contact in contacts">
<span edit-in-place="" ng-bind="contact.number"></span> |
<span edit-in-place="" >{{contact.name}}</span>
</li>
</ul>
<br />
<p>Here we repeat the contacts to ensure bindings work:</p>
<br />
<ul>
<li ng-repeat="contact in contacts">
{{contact.number}} | {{contact.name}}
</li>
</ul>
</div>
</div>
var app = angular.module( 'myApp', [] );
app.directive( 'editInPlace', function() {
return {
restrict: 'A',
//scope: { contact:"=" },
template: '<span ng-click="edit()" ng-bind="value"></span><input ng-model="value"></input>',
link: function ( $scope, element, attrs ) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element( element.children()[1] );
// This directive should have a set class so we can style it.
element.addClass( 'edit-in-place' );
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass( 'active' );
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
// When we leave the input, we're done editing.
inputElement.prop( 'onblur', function() {
$scope.editing = false;
element.removeClass( 'active' );
});
}
};
});
app.controller('ContactsCtrl', function ( $scope ) {
$scope.contacts = [
{ number: '+25480989333', name: 'sharon'},
{ number: '+42079872232', name: 'steve'}
];
});
You are running into problems because you are misusing angular.
First, a directive should be self-contained, but you are pulling functionality out of it, which makes it less universal and less reusable. In your code, you have functionality in the DOM and in the controller that belongs in the directive. Why?
Second, it's also unclear from your markup and javascript specifically want you want to accomplish when all these pieces are strung together.
Third, in most cases, directives should have their own isolated scope, which is done by declaring a scope object with attributes it should bind. You shouldn't be passing an expression (i.e. {{contact.name}}) inside the directive as it will break the binding and your contact will not be updated when the edit-in-place finishes. The proper way is to establish bi-directional binding through an = property on the scope. ng-bind isn't what you want here: that's scope-specific, so we use it inside the directive's scope. As Valentyn suggested, you could do some magic to get around this, but it's not a good idea and it's super-simple to set it up the right way. What's the issue with doing this by an attribute?
This is all bad Ju-ju.
As I pointed out in your other question on this same topic, you must make your directive self-contained and work with angular, rather than against it. Here's an attribute-based version of the fiddle I gave you previously, meeting the first of your requirements. Please let me know what is wrong specifically with this implementation and we can talk about the angular way of fixing it.
Lastly, if you provide further context on what you need in terms of a "button", I'll incorporate that into the fiddle too.
[update]
It is possible to make the directives work your way, but you will run into problems eventually (or right now, it would seem). All components in an angular app (or any app for that matter) should be as self-contained as is feasible. It's not a "rule" or limitation; it's a "best practice". Similarly, communication between directive components can occur through a controller, but it shouldn't. Ideally, you shouldn't reference the DOM in a controller at all - that's what directives are for.
If your specific purpose is a row that is editable, then that is your directive. It's okay to have a lower-level generic edit-in-place directive that the larger directive uses, but there is still the higher-level directive too. The higher-level directive encapsulates the logic between them. This higher-level component would then require a contact object.
Lastly, no, there isn't necessarily a big difference between ng-bind="var" and {{var}}. But that's not the issue; the issue was where that binding takes place. In your example, a value was passed to the directive instead of a bi-directionally-bound variable. My point was that the directive needs access to the variable so it can change it.
Summary: You are coding in a very jQuery-style way. That's great for coding in jQuery, but it doesn't work so well when coding in Angular. In fact, it causes a lot of problems, like the ones you're experiencing. In jQuery, you would, for example, dynamically insert DOM elements, declare and handle events, and manually bind variables all within a single code block, all manually. In Angular, there is a clean separation of concerns and most of the binding is automatic. In most cases, it leads to javascript code at least two-thirds smaller than the jQuery alternative. This is one of those cases.
That said, I have created a Plunker that contains a more sophisticated version of both the edit-in-place as well as a new higher-level directive to incorporate additional features: http://plnkr.co/edit/LVUIQD?p=preview.
I hope this helps.
[update 2]
These are the answers to your new round of questions. They may be good for your edification, but I already gave you the "angular way" to fix your problem. You will also find that I already addressed these questions (in broader strokes) earlier in my original answer as well as in my update. Hopefully, this makes it more apparent.
Question: "Comenting out in the directive: template and scope -> contact.number and contact.name are displayed"
My Reply: When you do not specify a scope, the directive inherits its parent scope. You bound and interpolated the name and number within the context of the parent, so it "works". Because the directive will alter the value, however, this is not a good way way to solve it. It really should have its own scope.
Question: "Comenting out in the directive: scope -> contact.number only is displayed"
My Reply: You bound a scope property of the parent to the "contact.number" directive, so it will get placed inside during the $digest loop - after the directive has been processed. On the "contact.name", you put it inside the directive, which can only work if the directive codes for transclusion.
Question: "Not commenting out anything -> nothing is displayed"
My Reply: Right. If the directive has its own scope (and this one definitely should), then you must use a defined directive scope property to communicate values, as my several code samples demonstrate. Your code, however, tries to use the parent scope in the directive when we explicitly forbid that by using the scope property in its definition.
Summary: While this second update may be informative (and I hope that it is), it doesn't answer the question beneath your questions: how do I use angular components correctly so that the scope I'm using is always what I think it is? My first post and the subsequent update, answer that question.
Here is little bit updated your fiddle, but it need further improvements to meet full list of your requirements: http://jsfiddle.net/5VRFE/
Key point is:
scope: { value:"=editInPlace" },
Some notes: its better to use ng-show ng-hide directivies for visual appearing-hiding instead of changing css classes. Also its better to spread functionality into different directives to have better separation of concerns (check ngBlur directive)
About your confusion of scope check guide about scopes paragraph "Understanding Transclusion and Scopes": Each directive have separate isolated scopes, if you want to have access from directive's template to controller's scope use directive scope binging ("scope" field of directive definition object). And also transcluded elements have a scope of from where you defined transcluding template.
From the first view those isolated scope sounds little bit strange, but when you have good structured directives (note also that one directive can require another and share bindings) you can find it extremly usefull.