I have the following directive
courseApp.directive("courseoverview", function() {
function link(scope, element, attrs) {
scope.switched = false;
//hover handler
scope.hoverItem = function(hovered){
if (hovered) {
//element.addClass('hover');
$('#course-'+ scope.$index +' figure').addClass('tint');
//console.log(scope.$index);
}
else
//element.removeClass('hover');
$('#course-'+ scope.$index +' figure').removeClass('tint');
};
}
return {
restrict : 'A',
replace: true,
transclude: true,
templateUrl: "views/course-overview.html",
link: link
}});
The directive is called with the following code
<li data-ng-repeat="item in social" class="social-{{item.name}}"
ng-mouseover="hoverItem(true);"
ng-mouseout="hoverItem(false);"
current-social="{{item.name}}">
The hover function works great but i need access to this attribute in the directive, well i need the value of it.
Any ideas on how to achieve this would be great.
First, your directive is named 'courseoverview' but I don't see that attribute anywhere in the markup you provided.
To address your question, you say I need access to this attribute in the directive. But I think you have the attributes in the link function - see the attrs parameter. Those are the attributes on the element that fired your directive.
function link(scope, element, attrs) { ... }
See this answer for more.
Related
I'm in need of building a transformation directive that transforms custom directives into html.
Input like: <link text="hello world"></link>
should output to: <a class="someclass" ng-click="linkClicked('hello world')"></a>
linkClicked should be called on the parent controller of the directive.
It would have been very easy if I was the one responsible for the html holding the 'link' directive (using isolated scope), but I'm not. It's an as-is input and I have to figure a way to still do it.
There are countless examples on how to do similar bindings using the default scope of the directive, but I'm writing my controllers using John Papa's recommendations with controllerAs, but don't want to create another instance on the controller in the directive.
This is what I have reached so far:
(function () {
'use strict';
angular
.module('app')
.directive('link', link);
link.$inject = ['$compile'];
function link($compile) {
return {
restrict: 'E',
replace: true,
template: '<a class="someclass"></a>',
terminal: true,
priority: 1000,
link: function (scope, element, attributes) {
element.removeAttr('link'); // Remove the attribute to avoid indefinite loop.
element.attr('ng-click', 'linkClicked(\'' + attributes.text + '\')');
$compile(element)(scope);
},
};
}
})();
$scope.linkClicked = function(text){...} in the parent controller works.
element.attr('ng-click', 'abc.linkClicked(..)') in the directive (where the parent's controllerAs is abc) - also works.
The problem is I don't know which controller will use my directive and can't hard-code the 'abc' name in it.
What do you suggest I should be doing?
It's difficult to understand from your question all the constraints that you are facing, but if the only HTML you get is:
<link text="some text">
and you need to generate a call to some function, then the function must either be:
assumed by the directive, or
conveyed to the directive
#1 is problematic because the user of the directive now needs to understand its internals. Still, it's possible if you assume that a function name is linkClicked (or whatever you want to call it), and the user of your directive would have to take special care to alias the function he really needs (could be done with "controllerAs" as well):
<div ng-controller="FooCtrl as foo" ng-init="linkClicked = foo.actualFunctionOfFoo">
...
<link text="some text">
...
</div>
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
link: function(scope, element, attrs, ctrl){
var template = '<a class="someclass" ng-click="linkClicked(\'' +
attrs.text +
'\')">link</a>';
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
Demo
#2 is typically achieved via attributes, which isn't possible in your case. But you could also create a sort of "proxy" directive - let's call it onLinkClick - that could execute whatever expression you need:
<div ng-controller="FooCtrl as foo"
on-link-click="foo.actualFunctionOfFoo($data)">
...
<link text="some text">
...
</div>
The link directive now needs to require: "onLinkClick":
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
scope: true,
require: "?^onLinkClick",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
var template = '<a class="someclass" ng-click="localClick()">link</a>';
scope.localClick = function(){
ctrl.externalFn(attrs.text);
};
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
app.directive("onLinkClick", function($parse){
return {
restrict: "A",
controller: function($scope, $attrs){
var ctrl = this;
var expr = $parse($attrs.onLinkClick);
ctrl.externalFn = function(data){
expr($scope, {$data: data});
};
},
};
});
Demo
Notice that having a link directive would also execute on <link> inside <head>. So, make attempts to detect it and skip everything. For the demo purposes, I used a directive called blink to avoid this issue.
I have a directive that is restricted to attribute. I want to do either of 2 things. If a certain condition is met, I want to use the children for the element with the directive attribute as the model (thats the content) or if that condition is not met then I want to data bind from a service instead, so the directive would replace the children with something i was given. I have a directive that is doing the latter but Im finding it very hard to have it grab the children before the compiler comes in and replaces it with its template... Anyone know how this is done if its possible?
I think what you're looking for is element.context in your directive's link (or compile) function.
Inside your link function (pre or post), the original element that your directive was found on is stored in the passed-in element's context property. So if your service call returns no data, you can just replace the compiled element with the original element by doing element.replaceWith(element.context).
So your directive would look something like this:
.directive('ttContent', ['mySvc', function (mySvc) {
return {
restrict: 'A',
replace: true,
transclude: false,
template: '<div class="content new-content" ng-bind-html="htmlContent | sanitize"></div>',
scope: {
testDataReturned: '#'
},
link: {
pre: function (scope, element, attrs) {
},
post: function (scope, element, attrs){
mySvc.fetchContent().then(function success(data){
if (data) {
scope.htmlContent = data;
} else {
// element.context is our original, pre-compiled element
element.replaceWith(element.context);
}
}, function fail(data){
element.replaceWith(element.context);
});
}
}
};
}]);
Here's a plunk.
Can someone help me with this problem?
Let's say I have a custom directive called mycontrol. I want to pass all the attributes in this directive to a control used inside my directive template. Here is an example -
<mycontrol data-var1="value1" data-var2="value2"..../>
My template is - <input/>
How do I define the directive definition object to get the output as
<input data-var1="value1" data-var2="value2"...>
Please note that the attributes number and names are not fixed.
All of the attributes should be carried over.
Look at this fiddle: http://jsfiddle.net/XgA6T/
Open the console and look at the element that is logged.
The directive looks like this:
myApp.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<input>',
link: function(scope, elem, attrs) {
console.log(elem);
var domElem = elem[0];
if (domElem.dataset && domElem.dataset != null) {
console.log(domElem.dataset);
scope.values = domElem.dataset;
}
}
};
});
I have two directive
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: 'element',
compile: function (element, attr, linker) {
return function (scope, element, attr) {
var parent = element.parent();
linker(scope, function (clone) {
parent.prepend($compile( clone.children()[0])(scope));//cause error.
// parent.prepend(clone);// This line remove the error but i want to access the children in my real app.
});
};
}
}
});
app.directive('panel', function ($compile) {
return {
restrict: "E",
replace: true,
transclude: true,
template: "<div ng-transclude ></div>",
link: function (scope, elem, attrs) {
}
}
});
And this is my view :
<panel1>
<panel>
<input type="text" ng-model="firstName" />
</panel>
</panel1>
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <div class="ng-scope" ng-transclude="">
I know that panel1 is not a practical directive. But in my real application I encounter this issue too.
I see some explanation on http://docs.angularjs.org/error/ngTransclude:orphan. But I don't know why I have this error here and how to resolve it.
EDIT
I have created a jsfiddle page. Thank you in advance.
EDIT
In my real app panel1 does something like this:
<panel1>
<input type="text>
<input type="text>
<!--other elements or directive-->
</panel1>
result =>
<div>
<div class="x"><input type="text></div>
<div class="x"><input type="text></div>
<!--other elements or directive wrapped in div -->
</div>
The reason is when the DOM is finished loading, angular will traverse though the DOM and transform all directives into its template before calling the compile and link function.
It means that when you call $compile(clone.children()[0])(scope), the clone.children()[0] which is your <panel> in this case is already transformed by angular.
clone.children() already becomes:
<div ng-transclude="">fsafsafasdf</div>
(the panel element has been removed and replaced).
It's the same with you're compiling a normal div with ng-transclude. When you compile a normal div with ng-transclude, angular throws exception as it says in the docs:
This error often occurs when you have forgotten to set transclude:
true in some directive definition, and then used ngTransclude in the
directive's template.
DEMO (check console to see output)
Even when you set replace:false to retain your <panel>, sometimes you will see the transformed element like this:
<panel class="ng-scope"><div ng-transclude=""><div ng-transclude="" class="ng-scope"><div ng-transclude="" class="ng-scope">fsafsafasdf</div></div></div></panel>
which is also problematic because the ng-transclude is duplicated
DEMO
To avoid conflicting with angular compilation process, I recommend setting the inner html of <panel1> as template or templateUrl property
Your HTML:
<div data-ng-app="app">
<panel1>
</panel1>
</div>
Your JS:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>",
}
});
As you can see, this code is cleaner as we don't need to deal with transcluding the element manually.
DEMO
Updated with a solution to add elements dynamically without using template or templateUrl:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<div></div>",
link : function(scope,element){
var html = "<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>";
element.append(html);
$compile(element.contents())(scope);
}
}
});
DEMO
If you want to put it on html page, ensure do not compile it again:
DEMO
If you need to add a div per each children. Just use the out-of the box ng-transclude.
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div><div ng-transclude></div></div>" //you could adjust your template to add more nesting divs or remove
}
});
DEMO (you may need to adjust the template to your needs, remove div or add more divs)
Solution based on OP's updated question:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div ng-transclude></div>",
link: function (scope, elem, attrs) {
elem.children().wrap("<div>"); //Don't need to use compile here.
//Just wrap the children in a div, you could adjust this logic to add class to div depending on your children
}
}
});
DEMO
You are doing a few things wrong in your code. I'll try to list them:
Firstly, since you are using angular 1.2.6 you should no longer use the transclude (your linker function) as a parameter to the compile function. This has been deprecated and should now be passed in as the 5th parameter to your link function:
compile: function (element, attr) {
return function (scope, element, attr, ctrl, linker) {
....};
This is not causing the particular problem you are seeing, but it's a good practice to stop using the deprecated syntax.
The real problem is in how you apply your transclude function in the panel1 directive:
parent.prepend($compile(clone.children()[0])(scope));
Before I go into what's wrong let's quickly review how transclude works.
Whenever a directive uses transclusion, the transcluded content is removed from the dom. But it's compiled contents are acessible through a function passed in as the 5th parameter of your link function (commonly referred to as the transclude function).
The key is that the content is compiled. This means you should not call $compile on the dom passed in to your transclude.
Furthermore, when you are trying to insert your transcluded DOM you are going to the parent and trying to add it there. Typically directives should limit their dom manipulation to their own element and below, and not try to modify parent dom. This can greatly confuse angular which traverses the DOM in order and hierarchically.
Judging from what your are trying to do, the easier way to accomplish it is to use transclude: true instead of transclude: 'element'. Let's explain the difference:
transclude: 'element' will remove the element itself from the DOM and give you back the whole element back when you call the transclude function.
transclude: true will just remove the children of the element from the dom, and give you the children back when you call your transclude.
Since it seems you care only about the children, you should use transclude true (instead of getting the children() from your clone). Then you can simply replace the element with it's children (therefore not going up and messing with the parent dom).
Finally, it is not good practice to override the transcluded function's scope unless you have good reason to do so (generally transcluded content should keep it's original scope). So I would avoid passing in the scope when you call your linker().
Your final simplified directive should look something like:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.replaceWith(clone);
});
}
}
});
Ignore what was said in the previous answer about replace: true and transclude: true. That is not how things work, and your panel directive is fine and should work as expected as long as you fix your panel1 directive.
Here is a js-fiddle of the corrections I made hopefully it works as you expect.
http://jsfiddle.net/77Spt/3/
EDIT:
It was asked if you can wrap the transcluded content in a div. The easiest way is to simply use a template like you do in your other directive (the id in the template is just so you can see it in the html, it serves no other purpose):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv' ng-transclude></div>"
}
});
Or if you want to use the transclude function (my personal preference):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv'></div>",
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.append(clone);
});
}
}
});
The reason I prefer this syntax is that ng-transclude is a simple and dumb directive that is easily confused. Although it's simple in this situation, manually adding the dom exactly where you want is the fail-safe way to do it.
Here's the fiddle for it:
http://jsfiddle.net/77Spt/6/
I got this because I had directiveChild nested in directiveParent as a result of transclude.
The trick was that directiveChild was accidentally using the same templateUrl as directiveParent.
I'm trying to write a directive to create a map component so I can write something like:
<map></map>
Now the directive looks like this:
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.$watch('selectedCenter', function() {
renderMap(scope.selectedCenter.location.latitude, scope.selectedCenter.location.longitude, attrs.zoom?parseInt(attrs.zoom):17);
});
function renderMap(latitude, longitude, zoom){
GoogleMaps.setCenter(latitude, longitude);
GoogleMaps.setZoom(zoom);
GoogleMaps.render(element[0]);
}
}
};
});
The problem is that 'watch' inside the directive doesn't looks very well thinking in the reusability of the component. So I guess the best thing is being able to do something like:
<map ng-model="selectedCenter.location"></map>
But I don't know if it's even a good thing using angular directives inside custom directives or how can I get the object indicated in the ng-model attribute in the custom-directive's link function.
You will need to do something like that
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
scope: {
ngModel: '=' // set up bi-directional binding between a local scope property and the parent scope property
},
as of now you could safely watch your scope.ngModel and when ever the relevant value will be changed outside the directive you will be notified.
Please note that adding the scope property to our directive will create a new isolated scope.
You can refer to the angular doc around directive here and especially the section "Directive Definition Object" for more details around the scope property.
Finally you could also use this tutorial where you will find all the material to achieve a directive with two way communication form your app to the directive and opposite.
Without scope declaration in directive:
html
<map ng-model="selectedCenter"></map>
directive
app.directive('map', function() {
return {
restrict: 'E',
require: 'ngModel',
link: function(scope, el, attrs) {
scope.$watch(attrs.ngModel, function(newValue) {
console.log("Changed to " + newValue);
});
}
};
});
One easy way you can achieve this would be to do something like
<map model="selectedCenter"></map>
and inside your directive change the watch to
scope.$watch(attrs.model, function() {
and you are good to go