Duplicate attributes when using directive compile with transclude - angularjs

JsFiddle of the issue: http://jsfiddle.net/UYf7U/
When using the angularJs transclude within a directives compile, it will duplicate any
attribute properties. I.e.
<a class="myClass">my link</a>
Will become
<a class="myClass myClass">my link</a>
Similarly, when using an ngClick
<a ng-click="myFunction()"> my link</a>
Will become
<a ng-click="myFunction() myFunction()"> my link</a>
The fiddle demonstrates this, and unfortunately it crashes. It's a stripped down version of what I'm trying to implement.
Is there a way around this? I've posted the issue to github to: https://github.com/angular/angular.js/issues/2576
When clicking on Hello the word "clicked" should appear in an alert.

This happens because myDirective is being initialized twice - first as part of your markup:
<div class="transcludeMe">
<div data-transclude-this="here">
<div class="myDirective"></div>
</div>
</div>
Second in the transcludeMe directive - since you do this in the compile stage of the directive initialization:
transcludeHere[0].innerHTML = clone[x].innerHTML
Since you use replace:true all attributes of the original element will get copied to the template element. If you remove this your example works, but you still be aware that myDirective is getting initialized two times: http://jsfiddle.net/tkzgG/

How important is it to you to specify the directive name as a class? This issue does not occur when the directives are used as elements directly.
See http://jsfiddle.net/smmccrohan/cfP3U/
Like thus, plus replacing the restrict: 'C' with restrict: 'E' in the directive definitions (and making some case changes to avoid an issue there):
<div ng-app="app">
<div ng-controller="ParentCtrl">
<transcludeme>
<div data-transclude-this="here">
<mydirective />
</div>
</transcludeme>
</div>
</div>

I found a different way to do multi-transclusion and that fixed my problem entirely, here's the updated fiddle for my problem being fixed: http://jsfiddle.net/UYf7U/1/
The code came from my previous question here: Multiple transclusions of separate html in an update that I did not see.
The fiddle will be out of date, but this is my final multi transclusion function. I've mode the logic into compile instead of the controller so that it can transclude dom that needs to have things like ng-repeat
.directive('multiTranscludeTo', function($rootScope){
return {
compile: function(tElement, tAttributes, transclude){
var baseScope = this;
transclude($rootScope, function(clone){
for (var x = 0; x < clone.length; x++){
var child = angular.element(clone[x]);
var viewName = child.attr('data-multi-transclude-from') || child.attr('multi-transclude-from');
if (viewName && viewName.split(" ")[0] == tAttributes["multiTranscludeTo"]){
tElement.html(clone[x].innerHTML);
}
}
});
}
}
})

Related

Hide/show an element in Angularjs using custom directive, ng-show and $scope

When a link is clicked in the app navigation a dropdown with ui-view content shows below each respective link.
The HTML:
<div class="sc-dash-header">
<ul>
<li>
<a class="navbar-brand" show-nav-popup href="">download</a>
<div id="nav-download-progress" class="dash-hdr-popup" ng-show="showPopup">
<div ui-view="hdr-download-progress"></div>
</div>
</li>
<li>
<a class="navbar-brand" show-nav-popup href="">add</a>
<div id="nav-add" class="dash-hdr-popup" ng-show="showPopup">
<div ui-view="hdr-add-content"></div>
</div>
</li>
<li>
<a class="navbar-brand" show-nav-popup href="">enter pin</a>
<div id="nav-unlock" class="dash-hdr-popup" ng-show="showPopup">
<div ui-view="hdr-unlock"></div>
</div>
</li>
</ul>
</div>
I've included an ng-show attribute to open the dropdown when $scope.showPopup is set to true.
To achieve this I've created a custom directive with an on click called show-nav-popup.
The JS:
.directive('showNavPopup', function () {
return {
restrict: 'A',
// scope: {},
link: function(scope, el, attrs) {
el.on('click', function(){
scope.$apply(function () {
scope.showPopup = true;
});
console.log(scope);
});
}
};
});
The above works, but the dropdown opens on each element.
Question: I need to isolate the scope, so on each click, only the respective dropdown appears. I uncomment the line // scope: {} - but this doesn't work..
Angularjs n00b here - any help would be much appreciated
Having an isolate scope in this situation wouldn't fix the problem. There are a ton of ways to achieve what you want though. One of which is to assign each show-popup-nav an id, turn $scope.showPopup into an array, and keep an individual true/false for each id. Then for each ng-show, you look at the index corresponding to each id for the true/false value.
I coded it up on that guy's Plunker, working as you expect: http://plnkr.co/edit/CSikLIiuPNT9dfsfZfLk
EDIT: I should say, you COULD use an isolate scope to fix this, but that would require a lot of changes to your DOM, as the ng-show directive is a sibling to your show-popup-nav, and not a child.
When you create the isolate scope, the scope applies to the element that your directive is applied to, and it's child elements. In this case that's just the anchor tag:
<a class="navbar-brand" show-nav-popup href="">download</a>
You are using an ng-show on a tag that is a sibling to the anchor tag:
<div id="nav-download-progress" class="dash-hdr-popup" ng-show="showPopup">
The sibling is not part of the isolate scope, and so it never notices that the value of showPopup has changed.
The ng-show would work if it were applied to a DOM element that was a child of the anchor tag.
EDIT
One way to make this work would be to wrap your two siblings in a parent tag, and use the directive on the parent:
<div show-nav-popup>
Download
<div ng-show="showPopup"></div>
</div>
Then you'd need to modify your directive's code to find the anchor tag and apply the click handler.
You might instead try a completely different approach as suggest in the other answer by #Bill Bergquist

ng-repeat inside compiled html for directive

I have two directives:
window.app.directive('placeholder', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Store the placeholder element for later use
$rootScope["placeholder_" + attr.placeholder] = element[0];
// Clear the placeholder when navigating
$rootScope.$on('$routeChangeSuccess', function (e, a, b) {
element.html('');
});
}
};
});
window.app.directive('section', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Locate the placeholder element
var targetElement = $rootScope["placeholder_" + attr.section];
// Compile the template and bind it to the current scope, and inject it into the placeholder
$(targetElement).html($compile(element.html())(scope));
element.html('');
}
};
});
I use them to basically swap out one section with html in another.
If I have the following html:
<div placeholder="footer"></div>
<div section="footer">
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
The ng-repeat doesn't seem to be working. If I simply output {{items}} below the , it displays fine. Also, I know binding is working because I can change items and it will update.
Lastly, if I move the ul outside the section it works fine.
So, my question is why does this not work (compile ng-repeat inside directive).
Am I missing something?
EDIT:
What is confusing me, is I can do this:
<div section="footer">
<!-- This Works -->
{{items}}
<!-- This also works -->
<input type="text" ng-model="items[0].Description" />
<!-- This doesn't work -->
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
This isn't going to work. It can't evaluate something from another scope without having an exact copy of it in its scope. If you want two directives to communicate use require and setup a way for them to do that if they aren't in a parent child relationship.
A couple of things you should think about. Essentially what you are doing is called transclusion. Section directive would use ng-transclude to capture the client's defined code. Use transclusion and maybe you can evaluate the template into html in the scope of section then using directive communication allow it to pass the HTML block (already evaluated) to the other directive. The only problem is making sure this happens when things change through binding. You're probably going to need some $watches on variables in section in order for placeholder to be notified when things change.
You will probably need a 3rd directive so allow section and placeholder to communicate through. In this example say I have a 3rd directive called broadcaster. Then section and placeholder will require broadcaster (ie require: '^broadcaster'), and it will define some interface for each of the directives to send HTML from section -> placeholder. Something like this:
<div broadcaster>
<div placeholder="footer"></div>
<div section="footer">
<ul>...transcluded content</ul>
</div>
</div>

ng-repeat with ng-include not working

I am trying to use an ng-repeat that includes an ng-include. The problem is that the first element in the ng-repeat is just the ng-include template with none of the data from the ng-repeat filled in. Is there a way I can somehow bind the template from the ng-include so it works on the first ng-repeat?
<div ng-repeat="item in items">
<div ng-include src="'views/template.html'"></div>
</div>
For example, if my ng-repeat contains 10 items, then the first item that is rendered will just be the empty template. Items 2-10 WILL be rendered as they should be. What am I doing wrong?
First make sure that the data that is contained in the first index of items actually has the data that you want.
One possible solution to your problem would be to simply not show the first index of the ng-repeat:
<div ng-repeat="item in items" ng-show="!$first">
<div ng-include src="'views/template.html'"></div>
</div>
This may not actually tackle the root of your problem, but it may still get your application working a bit more like what you expect.
Another possible solution:
<div ng-repeat="item in items" ng-include="'views/template.html'"></div>
see example here:
http://plnkr.co/edit/Yvd73HiFS8dXvpvpEeFu?p=preview
One more possible fix just for good measure:
Use a component:
html:
<div ng-repeat="item in items">
<my-include></my-include>
</div>
js:
angular.module("app").directive("myInclude", function() {
return {
restrict: "E",
templateUrl: "/views/template.html"
}
})
I ran into the same problem, and finally figured out that the first element has not been fetched and compiled in time for the first ng-repeat iteration. Using $templateCache will fix the problem.
You can cache your template in a script tag:
<script type="text/ng-template" id="templateId.html">
<p>This is the content of the template</p>
</script>
Or in your app's run function:
angular.module("app").run(function($http, $templateCache) {
$http.get("/views/template.html", { cache: $templateCache });
});
You can also use $templateCache inside your directive, although it's a bit harder to setup. If your templates are dynamic, I would recommend creating a template cache service. This SO question has some good examples of template caching inside a directive and a service:
Using $http and $templateCache from within a directive doesn't return results
Using a directive worked for me: https://stackoverflow.com/a/24673257/188926
In your case:
1) define a directive:
angular.module('myApp')
.directive('mytemplate', function() {
return {
templateUrl: 'views/template.html'
};
});
2) use your new directive:
<mytemplate />
... or if you're concerned about HTML validation:
<div mytemplate></div>

Angular Directive not replacing Element

I'm trying to create a directive that will work against xml that i am injecting into the dom via a service.
Here is my a relatively reduced example, having removed the async service call for data and also the template: jsBin - transforming elements using directive
Everything works well with regard getting my post elements' header attribute into an h2 tag but it is leaving a element within my page which will fail for some browsers.
to clarify, this is what i get:
<post class="ng-isolate-scope ng-scope" heading="test heading">
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
</post>
and this is what i would expect:
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
I think Adam's answer is the better route, but alternatively and easier if you include jquery you can do this in your link function:
var e =$compile(template)(scope);
element.replaceWith(e);
You aren't using template correctly in your directive. Your link function should not applying your template as you are in the example code.
var linker = function(scope, element, attrs) {
var template = getTemplate();
element.html(template);
$compile(element.contents())(scope);
};
Instead of that, just do this:
return {
restrict: 'E',
replace: true,
scope: {
heading: '#'
},
template: '<div><h2>{{heading}}</h2><div>test</div></div>'
};
In your post directive. 'replace: true' will not impact anything if you are compiling and manipulating the DOM yourself.
At the same time, though, I have no idea what your compile directive is for and why you have a factory that returns an HTML string. All of that code looks like it could be reduced to a single directive. I can't really comment on what you're trying to do, but once you start using $compile all over the place, odds are you aren't doing things the 'Angular way'.

AngularJs - Access to DOM element inside ng-repeat

I have the next template:
<div ng-repeat="friend in friends | filter:filterFriendsHandler">
{{friend.name}}
</div>
and in my controller i have:
$scope.filterFriendsHandler = function(friend){
//HERE I WANT TO ACCESS TO FRIEND DOM ELEMENT; to do something like this:
//$(friendElement).text('foo');
}
Thanks
I'm going to answer the specific question here, yes I understand this isn't the "angular" way of doing things. If you want to do things the "correct" way, then don't do this, use a directive. There, disclaimers aside, here's how to do it:
Basically, what you want to do is give the DOM element an ID based on the $index or a unique value in your ng-repeat object. Here, I'll just use $index.
<div ng-repeat="friend in friends" id="friend_{{$index}}" ng-bind-html="doSomethingBadToTheDom('friend_' + $index)">
{{friend.title}}
</div>
Then, inside your controller, just query the DOM for the element with that ID:
$scope.doSomethingBadToTheDom = function(ele_id) {
var element = document.getElementById(ele_id);
element.innerHTML = "I'm abusing angular";
}
We're using ng-bind-html here because the DOM element will exist when your controller function executes, in the case of something like ng-init, it won't.
Again, this goes against everything angular stands for, so if you're trying to follow angular best practices, don't do this.
I've run into situations where the technique is useful though, especially when dealing with non-angular libraries, or those times when the "angular way" is more trouble than it's worth.
You need to use a directive for that
<div ng-app="test-app" ng-controller="MyController">
<div ng-repeat="friend in friends" nop>
{{friend.title}}
</div>
</div>
JS
app.directive('nop', function(){
return {
link: function(scope, elm){
console.log('eee', elm, arguments);
elm.css('color', 'red');
}
}
});
demo: Fiddle

Categories

Resources