I am trying to define a directive to show a checkmark when a question has been answered.
questModule.directive('showCheckmarkOnDone', function () {
return {
transclude: true,
template: "<div ng-transclude></div><img src='img/checkmark.svg' " +
"ng-show='scope.questions[type][number-1].done' " +
"class='checkmark' style='height: {{height}}px; width: {{height}}px;'>",
link: function (scope, element, attrs) {
scope.height = $(".app").height()/12;
scope.number = attrs.number;
scope.type = attrs.type;
}
};
});
The scope.questions[type][number-1].done exists in my controller for the page, and I can see that it is being updated correctly when I press the done button. However, the directive does not register the change. I tried putting a $watch in the link function - that didn't help either. I think I'm a bit confused about how to get my directive scope to play nicely with my controller scope - any thoughts on how I can give this directive access to an object that exists in an outside controller? (scope.questions)
This is not a valid way to define directive scope:
scope: '#'
You can either (a) not define it, (b) set it to true, or (c) set it to {} (an object). See the docs for more info: http://docs.angularjs.org/guide/directive (find the header: "Directive Definition Object")
In this case, I imagine if you remove it, you may be OK, because it will allow scope.questions to be visible from your directive. If you reproduce the issue in jsfiddle.net or plnkr.co, it would be much easier to assist you.
Edit:
Your directive generally should have 1 parent element
You should not use scope in your directive's HTML, it's implied
I think you said as much in your comment, but you should strive to make your directive more generic by passing in scope.questions[0][0].done instead of looking it up in your directive's HTML using attributes.
Here's a working example: http://jsfiddle.net/EZy2F/1/
Related
When building angular directives, I've found there is more than one way to pass a value from the parent scope to the child scope. Some ways I'm aware of are:
Don't use an isolate scope at all, and the child will simply have
access to the parent scope (you can mount a pretty good argument that this is bad).
Use the attributes parameter of a link function.
Use an isolate scope and bind to the attribute (e.g. param: '=')
The codepen here: https://codepen.io/ariscol/pen/WEKzMe shows two similar directives, one done with link and one done with 2-way binding in an isolate scope. Furthermore, it shows how they differ as far as 1-time binding compared to 2-way binding. For reference, here are the two directives:
app.directive("contactWidgetWithScope", function() {
return {
restrict: 'E',
template: '<div ng-bind="contact.name"></div>'
+ '<div ng-bind="contact.title"></div>'
+ '<div ng-bind="contact.phone"></div>',
scope: {
contact: '='
}
};
});
app.directive("contactWidgetWithLink", function() {
return {
restrict: 'E',
template: '<div ng-bind="name"></div>'
+ '<div ng-bind="title"></div>'
+ '<div ng-bind="phone"></div>',
scope: {},
link: function(scope, elem, attrs) {
scope.name = attrs.contactname;
scope.title = attrs.contacttitle;
scope.phone = attrs.contactphone;
}
};
});
Now, if I were trying to decide which way was "better", I might consider how I was going to use this directive. If I was going to have a thousand contacts, and I wanted to use this directive to list all one thousand contacts on a page, in an ng-repeat, for example, I imagine that I would have significantly better performance with link, as it won't add any watchers. On the other hand, if I wanted this directive to be incorporated into a page header, and I wanted the contact details to be updated as you clicked on any given contact in a list, I would want 2-way binding, so that any change to some "selectedContact" property in a parent scope would be automatically reflected in this directive. Are those the proper considerations? Are there others?
To add to my confusion, it is simple to add an observer to a linked attribute and achieve a 1-way binding such that a change in the value of the attribute will be reflected in the child. Would doing this have more or less of a performance impact? Conversely, I imagine you could do a 1-time binding on the value of the scope version and thereby eliminate the performance impact, e.g.: <contact-widget-with-scope contact="::vm.contact">. That should work, right? Seems like that option gives you a lot of flexibility, because it means the person who invokes the directive can decide if they want to pay the performance price to get the benefit of 2-way binding or not. Are these considerations accurate? Are there other things I ought to consider when deciding how to make values available to my directives?
I am fairly new to Angular and still learning. I posted a question regarding how to pass scope data to directives. Two nice people answered my question but the explanations were very short, so I do not understand some of the code in two of the answers. I will post the two answers and accompanying code here, and if possible someone with be able to answer my questions.
1st set of Code
<li my-directive price="item.price" ng-repeat="item in products">{{item.name}} — {{item.price}}</li>
myApp.directive('myDirective', function(){
return {
scope: { price: '=' },
require: 'ngModel',
link : function(scope){
console.log(scope.price)
},
controller: function(scope, element, attrs, ngModel){
console.log(ngModel.price);
console.log(scope.price);
}
}
});
point 1
Why is scope declared in the directive ? Regarding 'isolated scope' property name has to be price, is there a reason for the variable name here? Can we give it a different name ?
point 2
I am new to ng. This is the first time I've noticed a 'controller declared in directive'. When and why do people declare a controller in a directive? Mostly we declare a controller outside of a directive. Are a controller function and a controller inside a directive both the same, or different?
point 3
What is the meaning of require: 'ngModel', ? If we do not write require: `ngModel what will fail to work?
2nd set of Code
angular.module('myApp')
.directive('myDirective', function () {
return {
transclude: true,
restrict: 'AEC',
scope: {
name: '=',
price: '='
},
templateUrl: 'my-directive.html',
link: function (scope, element, attr) {
}
}
}
});
<li my-directive ng-repeat="item in products" price="item.price" name = "item.name"></li>
points 1
What is transclude: true ? What is the meaning of transclude = true or false? In what kind of situation do people work using transclude: true or false?
points 2
Where I have to put file my-directive.html in folder, what would be the location of the file?
Will Angular load this template file automatically?
1st set
Point 1: why scope declared in directive ? isolated scope property name has to be price ? here can we give different name ?
Ans: scope is declared in directive so that each instance of directive will have their own scope independent of each other. No it need not be price, it can be any variable name as per your choice. It's a general practice to use variable names in context of directive's usage.
Point 2:
i am new in ng. this is first time i notice controller declared in directive. why and when people declare controller in directive ? mostly we declare controller out side of directive. controller function and controller inside in directive both are same or different?
Ans: This controller is the controller of your directive and is different than what you normally see for a module.
Point 3:
what is meaning of require: 'ngModel', ? if we do not write require: `ngModel then what will not work?
Ans: When using your directive, it forces it to be used along with the attribute/controller ng-model. You can refer this answer for more details.
2nd set
Point1: what is transclude: true ? what is the meaning of transclude = true or false? in what kind of situation people work with transclude: true or false?
Ans:It's setting transclude as true. As per documentation:
Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
transclude: true allows us to wrap a users template with our template. I've used it as true in a directive I wrote where if a table is empty, I show a error message showing No Data Available. In my case, table will not by displayed at all, instead this directive's template is displayed.
Points 2:
where i have to put the file my-directive.html in folder. what would be the location of the file?
angular will load this template file automatically?
Ans: You can place this template anywhere in the project folder. Like, in a folder named views or templates. As long as you provide correct path in templateUrl, angular will find the file and use it in the directive.
I implemented a directive that transcludes multiple fragments of child content into a template. It works but seems simpler than most of the examples I have seen, and raised a few questions about how transclusion works.
Here is the directive:
module.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: true,
replace: true,
scope: true,
template: '<div style="border: 1px solid {{color}}"><div class="my-transclude-title"></div><div class="my-transclude-body"></div></div>',
link: function(scope, element, attrs, controller, transclude) {
// just to check transcluded scope
scope.color = "red";
transclude(scope, function(clone, scope) {
Array.prototype.forEach.call(clone, function(node) {
if (! node.tagName) {
return;
}
// look for a placeholder node in the template that matches the tag in the multi-transcluded content
var placeholder = element[0].querySelector('.' + node.tagName.toLowerCase());
if (! placeholder) {
return;
}
// insert the transcluded content
placeholder.appendChild(node);
});
});
}
}
});
and here is example usage:
<div ng-controller="AppController">
<div my-transclude>
<my-transclude-title> <strong ng-bind="title"></strong>
</my-transclude-title>
<my-transclude-body>My name is {{name}} and I've been transcluded!</my-transclude-body>
</div>
</div>
You can see it in action in this fiddle.
Please notice a few things:
It matches fragments to template placeholders by element class, rather than explicitly defined child directives. Is there any reason to do this one way or another?
Unlike many examples I've seen, it doesn't explicitly use the $compile service on the child fragments. It seems like Angular is compiling the fragments after transclusion, at least in this simple case. Is this always correct?
It uses the (barely documented) transclude argument to the link function, rather than the other (barely documented) approach of injecting the $transclude local into the controller directive. After hearing so many admonitions not to manipulate DOM in controllers, the controller approach seems like an odd construct and it feels more natural to handle this in the link function. However, I tried it that way and it seems to work the same. Is there any difference between the two?
Thanks.
EDIT: to partially answer question #2, I discovered that you do need to explicitly compile transcluded content that is not cloned from the template where the directive was applied. See the difference in behavior here: http://jsfiddle.net/4tSqr/3/
To answer your question in regards to the differences between $transclude function in directive controller vs linking function, first we need understand that $transclude function can be accessed through directive compile, controller and linking functions.
UPDATE: According to 1.4 Angular documentation, compile(transclude) has been deprecated! So transclude function can only be accessible in your directive Controller or Linking function. (See official documentation for detail explanation)
There is a big difference when used $transclude in compile phase vs. $transclude in controller and linking phase due to during compiling phase, you don't have access to $scope vs. using in controller and linking functions where $scope (controller) and scope (linking) are accessible. With that said, the only difference in using $transclude in directive controller vs. linking is order of execution. For multiple nested directives, its relatively safe to use $transclude during your linking phase instead of using it in your controller.
The order goes like this:
parentDirectiveCompile -> childDirectiveCompile (Directive Compile)
parentDirectiveControllerPre, parentDirectiveControllerPost -> childDirectiveControllerPre, childDirectiveControllerPost (Directive Controller)
childLinkFunction -> parentLinkFunction
Notice how childLinkFunction gets executed first before parentLinkFunction? (Order of execution)
Helpful Resource:
Angular directives - when and how to use compile, controller, pre-link and post-link
Hopefully this answer can be helpful to you!
after some investigation:
After the release of Angular 1.20 pre-existing child nodes of a compiled directive that has a isolate scope will no longer inherit the new isolate scope because they have already been assigned to the parent scope. So... the built in transclude method that uses the attribute ng-transclude will only transclude templates to the desired location in this case but will not transclude pre-existing html to this location. This means that if you had a directive with an isolate scope and you wanted the html that was already there to be compiled to the new isolate scope you would need to use the transclude linker function inside of the directive.
You can see a working case of this problem here ui-codemirror placed within custom directives fails without an error
That fiddle illustrates the issue http://jsfiddle.net/LAqeX/2/
I want to create a directive that wraps a part of the page and hides it. And i would like to use ng-if to remove unnecessary bindings. But some black magic happens.
This is my preferable directive code.
app.directive('withIf', function(){
return {
restrict: 'E',
scope: {
title: '#'
},
transclude: true,
template: '<div>' +
'<p ng-click="visible = !visible">{{title}}</p>' +
'<div ng-if="visible" ng-transclude></div>'+
'</div>',
link: function(scope){
scope.visible = false;
}
}
});
It is supposed to create two scopes:
Directive isolate scope which has two variables - title and visible
Transcluded scope which prototypically inherits from "regular" scope tree.
However, ng-if makes transclued scope somewhat detached from reality and trasncluded scope does not inherit from controller. Please, see the fiddle, it illustrates the issue very clear.
Any ideas what is happening there and how to solve it?
UPDATE
It seems i have figured out reasons why scope chain looks broken. The scope created by ng-if belongs to withIf directive isolate branch. So it never knows that controller's scope even exists. But the question remains the same - how to use ng-if in such case.
ng-if creates a child scope, use $parent to access variables from parent scope. But in your case I would consider using ng-show or ng-hide instead of ng-if (they don't create child scopes)
This bug seems to be fixed in Angular 1.3 - https://github.com/angular/angular.js/pull/7499
I'm trying to get a custom directive to work inside of ngRepeat, but can't get the obvious to work. In this case I don't 'believe' I want to isolate scope. I suspect this is simply a matter of framework ignorance, but can't seem to figure it out. I have a plunk here to show: http://plnkr.co/edit/LNGJHtbh7Ay0CYzebcwr
The link function runs only once for each instance of the sel directive, so it renders the arr.name value one time. In order to make it aware of future changes, you can use a $watch:
link: function(scope, elm, attr){
scope.$watch('arr.name', function() {
elm.text(scope.arr.name)
});
}
Plunker here.
You can find more information on that in the $rootScope documentation.