Angular.js directive transclude ngRepeat - error - javascript

I'm trying to create a chart directive ( not interested in existing solutions ). I want to have definable templates for each bar using a sub-directive of bartemplate. I want to iterate the bartemplate so I thought using a template with ng-repeat and ng-transclude would do the trick ... instead angular threw an error.
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <div ng-repeat="bar in bars track by $index" ng-transclude="" class="ng-scope">
http://jsfiddle.net/jt4Y2/7/
So you may ask, why the transclude? I want to be able to include any element I add to the chart (ie. a label ) and not interfere with the <bartemplate>.

You have several problems with your code. First you are complicating it by doing transclusions withing transclusions within transclusions. Second, you are combining directives that already do transclusions such as ng-repeat and trying to transclude within it, which just won't work.
I have provided a simplified version of your code that does what you want:
http://jsfiddle.net/jt4Y2/6/
Let me try to explain what it is doing. First we have the barchart directive. I took the liberty of simplifying but just making the entire body act as the template for a bar rather than having a bar-template directive.
Because we have set transclude: true the actual contents inside the barchart div are stripped out of the dom, but made available through a transclude function that you can inject into the controller via the $transclude parameter. We take this function and save it on our controller, so we can access it later on.
The barchart directive is then replaced by this template:
<div xmlns="http://www.w3.org/2000/svg">
<div ng-repeat="bar in bars track by $index" render-bar></div>
</div>
Notice that there is no ng-transclude. Instead we create our own directive render-bar to render the template (this avoids any conflicts with ng-repeat and it's own transclusion).
The renderBar directive is pretty simple:
directive('renderBar', function(){
return {
require: '^barchart',
link: function(scope, element, attrs, controller){
controller.renderBarTemplate(scope, function(dom){
element.append(dom);
});
}
}
});
First, we require that there be a parent barchart directive. In the link function, we can then get its controller and access the stored transclude function which we named renderBarTemplate.
The first parameter we pass is the scope, which makes the function link against the current scope (basically the scope provided by ng-repeat which gives us access to the iteration variables including bar).
The compiled dom is returned via the a callback (the second paramter), which we can then attach to the <div> provided by ng-repeat.
Let me know if you have any questions or need additional clarification.
UPDATE:
Here's a version that let's you keep the <bartemplate> directive:
http://jsfiddle.net/jt4Y2/10/
I added the bartemplate directive:
directive('bartemplate', function(){
return {
transclude: true,
restrict: 'E',
require: '^barchart',
link: function(scope, element, attrs, barChartController, transclude){
element.remove();
barChartController.renderBarTemplate=transclude;
}
};
})
It is now responsible for taking its transclude function and storing it in the parent's controller. (so it can later be use by the render-bar directive.
Also notice the element.remove(). This is not absolutely necessary but is simply removes the remaning <bartemplate> tag from the html.

Related

Angularjs - is there a way to programatically click on an HTML <select> element?

I've seen a javascript solution that goes a little something like this:
var select = document.getElementById('selectId')
select.click();
Is there an AngularJS approach/best practice to the same thing? (Off the top of my head, you'd wrap the above code in an ng-click)
Yes there is. Here's the angular equivalent of what you have in JavaScript
angular.element('#selectId').trigger('click');
Working example
Any DOM manipulation in angular should occur inside of a directive.
View
<div id="selectId" clickMe>content</div>
Inside of a directive the link function triggers after the view is compiled. The second parameter in the link function is the element which the directive is placed on, this gives performance benefits since there is no need to traverse the dom. It is a JQlite element which you can directly call methods on.
Directive
app.directive('click-me', function(){
return{
link(scope, el, attr){
$(el).trigger('click');
}
}
});

Call a bound function within a Directive from an ng-click

I was wondering I am able to call a bound function within a directive when part of a div is clicked. I currently have a div with another div inside that serves as a button to expand the div. The large div has a directive that makes it expandable. I would like to click on the arrow in the corner of the div and have the arrow's ng-click call a bound function inside the expandable directive. Is this possible? If so, how can I implement it?
Here's a picture of the div:
<div expandable class="boxWithBorderStyle">
<div class="arrowStyle" ng-click="someFuncInsideExpandableDirective()"> </div>
</div>
The contents - i.e. the child elements - of the element hosting the directive have a different scope than the directive if the directive uses isolate scope (scope: {}), which I suspect is what you have.
In order to link the contents against the same scope, you'd need to manually transclude them:
.directive("expandable", function(){
return {
transclude: true,
scope: {
// whatever you have now
},
link: function(scope, element, attrs, ctrls, transcludeFn){
transcludeFn(scope, function(contentClone){
element.append(contentClone);
});
// here you can expose whatever functions you want on the isolate scope:
scope.someFuncInsideExpandableDirective = function(){
// do something
}
}
};
});
The first parameter of the transclusion function transcludeFn is the scope against which the transcluded content is linked, which, in this case, is the directive's scope.
Keep in mind, though, that it is a bit awkward from the point of view of the user of your directive, since now they have HTML that refers to some "magic" variables/function (which is of-course defined by your directive) that is not apparent to someone observing the HTML. Imagine you'd encounter some directive foo which did something similar:
<div foo="abc">
{{foobar}}
<button ng-click="doSomethingFoo()">do foo</button>
</div>
and foobar and doSomethingFoo were defined inside the scope of the foo directive - it would be more difficult to decipher without knowing the specifics of foo what was going on.

ng-repeat always rendering as last in directive template

I have a directive (<my-directive>) and child directives (<my-child-directive>) as below structure:
<my-directive>
<my-child-directive caption="{{student}}" ng-repeat="student in students"></my-child-directive>
<my-child-directive caption="static content1"></my-child-directive>
</my-directive>
The first child directive repeated using ng-repeat and The last child directive is a static item.
Now I have two questions here:
Question1:
In the final output, the last directive is rendering as first <li>. is there any way to render the <li>s in the same order of child directives?
Question2:
I have used a hidden div in <my-directive>'s template to render the transclude for temporary purpose. Is there any way to avoid this unwanted div?
here is my java script code:
app=angular.module('myApp', [])
app.controller("MyController",function($scope){
$scope.students=["Alex","George","Phillip"];
});
app.directive('myDirective',function(){
return{
restrict:'E',
transclude:true,
template:"<ul><li ng-repeat='item in items'>{{item.caption}}</li></ul> <div ng-transclude ng-hide='true'></div>",
controller:function($scope){
$scope.items=[];
this.addItem=function(subScope){
$scope.items.push(subScope);
}
},
link:function(scope,element,attrs){
}
};
});
app.directive('myChildDirective',function () {
return {
restrict: 'E',
require:"^myDirective",
scope:{
caption:"#"
},
link:function(scope,element,attrs,parentCtrl){
parentCtrl.addItem(scope);
}
}
})
fiddle: http://jsfiddle.net/fyds082s/5/
Can anybody help on this?
The link function of the second directive is executed before the directives that come before him. When you check the ng-repeat state while the static directive is executed it shows that the index is 0. it hasn't finished compiling the ng-repeat yet.
My guess is that angular links sibling element from last to first, the same way it links from child elements first
Update:
My guess is wrong. so my new guess it has something to do with child scopes. For example, when adding ng-if to the second directive it is indeed executed second: http://jsfiddle.net/fyds082s/7/
<my-directive>
<my-child-directive caption="{{student}}" ng-repeat="student in students"></my-child-directive>
<my-child-directive caption="static content1" ng-if="true"></my-child-directive>
</my-directive>
What order do you think the following would produce?
<my-directive>
<my-child-directive caption="one" ng-if="true"></my-child-directive>
<my-child-directive caption="two"></my-child-directive>
</my-directive>
If you answered two, one then hat's off to you.
The reason behind this somewhat unintuitive result is a "conditional rendering" directive like ng-if and ng-repeat. They both transclude their hosting element, but render it later - when $scope.$watch or $scope.$watchCollection fire an event, respectively - at which time the link function of myChildDirective is called.
The best approach is not to rely on the order of called link functions to determine the order of rendering.

How to dynamically nest a directive element in-between existing parent and child directive elements?

Background:
I am using a custom CMS where I have limited access to the code base. So, in a few cases I plan to make some DOM manipulations using JavaScript.
Problem:
I have a container directive and the container has plain old HTML items, but am not able to mark the items as being directives from the server side. Also, the plain old HTML items contain sub-content that are directives.
Example:
Here is the before:
DIV[container-directive]
DIV.some-item-in-html
DIV[some-directive-in-the-content]
DIV.some-item-in-html
DIV[some-directive-in-the-content]
...
Here is what the DOM should look like afterwards:
DIV[container-directive]
DIV[container-item] <-- This is what needs to be inserted
DIV.some-item-in-html
DIV[some-directive-in-the-content]
DIV[container-item] <-- This is what needs to be inserted
DIV.some-item-in-html
DIV[some-directive-in-the-content]
...
Question:
Does anyone have suggestions on the best approach to injecting DOM element that are directives in-between a nesting of directives using JavaScript?
Some thoughts:
I think manipulate the DOM in advance of the compilation by angular, but I wonder if there is a way to do this within Angular's framework.
Another option is from the container directive's post-linking function, I could wrap the HTML items in directive elements called "container-item" and then $compile the items manually. So, I tried this but I get an error related to the items already having directives inside with transcluded content. Something about the "ngTransclude" being unexpected. I think this is related to the inner directives already having been processed.
I would go with your first option and manipulate the DOM ahead of angular compilation.
You can do this within a directive that contains the elements that you want to manipulate.
For example:
app.directive('body', function() {
return {
restrict: 'E',
compile: function(element, attr) {
// find the inner element and wrap it
$('.some-item-in-html', element).wrap('<div class="container"></div>');
}
}
});
Parent directives are always compiled before child directives, so you can change the DOM of the children within the compile property, and not have to worry about recompiling or re-linking directives.
[EDIT]
Thanks to Biagio for the following edit.
This method shouldn't be used with a directive with a template because the element would be assigned to the template and not the child elements.
Another alternative is doing the DOM manipulation in a function that runs at the start of the angular lifecycle.
For example:
app.run(function(){
// find the inner element and wrap it
$('.some-item-in-html').wrap('<div class="container"></div>');
});

How to pass an updated value to a nested directive in AngularJS?

I have a directive with some boilerplate html, including a menu toggle button.
Within that directive (transcluded) is one of a number of different nested directives, each with it's own menu.
The outer directive has an isolated scope and the inner directive has no scope specified.
When I click the menu toggle button on the outer directive, I want to be able to show/hide the menu contained in the inner directive.
How do I communicate the change in the toggle value from the outer directive to the inner directive?
I have tried broadcasting the change in the outer directive and listening for it in the inner directive, but that doesn't seem to work.
I have tried requiring the outer directive in the inner directive - I can see the value of the toggle there, but don't see a way of watching it for changes.
Here's the link function from the outer directive:
link: function($scope, $element, $attrs){
var menuCollapsed = true;
$scope.toggleMenu = function(){
menuCollapsed = !menuCollapsed;
console.log("ToggleMenu",menuCollapsed);
$scope.$broadcast('menuCollapsed', menuCollapsed);
};
}
and the inner directive contains this:
link: function($scope, $element, $attrs, widgetCtrl){
$scope.$on('menuCollapsed', function(event, args){
console.log('menuCollapsed', event, args);
});
The log in the outer directive displays, but not the one in the inner directive.
if you use scope.emit the event will bubble up if you use scope.broadcast the event will be broadcasted down your tree.
I persoanly prefer events over watchers how ever if your child directive is a direct child of your parents directive scope, you can use standard data binding in your child dierctive attributes.
to require a parent directive in a child directive, the parent directive has to expose its API through its controller function.
those are hints for more detailed help i would need to see some code

Categories