Transclude in AngularJS without adding new element - javascript

Is there any way to transclude some content into a directive without adding extra elements.
For example
directive:
{
scope: {
someParam: "="
},
link: function(scope, element, attrs){
//do something
},
transclude: true,
template:'<div ng-transclude></div>'
}
source html:
<div my-directive some-param="somethingFromController">
my transcluded content: {{somethingElseFromController}}
</div>
With this example an extra div gets added to the markup. Normally this would be fine but I'm trying to use this directive inside a table so adding a div tag screws things up.
I also tried not specifying transclude or template which gets rid of the extra div tag but now {{somethingElseFromController}} cannot be found as the "transcluded" content is in an isolated scope. I know I could just get the parameters for my directive from the attrs object in the linking function instead of creating an isolated scope but I'd rather avoid needing to evaluate strings with scope.$apply().
Anyone know how to accomplish this?
Thanks!

What #Vakey answered is what I was searching for.
But as today, the Angular documentation says:
The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
So I used instead the controller (for the moment) and its $transclude function, as part of the example shown on the $compile documentation:
controller: function($scope, $element, $transclude) {
var transcludedContent, transclusionScope;
$transclude(function(clone, scope) {
$element.append(clone);
transcludedContent = clone;
transclusionScope = scope;
});
},

This actually is possible with Angular. Directives such as ng-repeat do this. Here is how you do it:
{
restrict: 'A',
transclude: true,
compile: function (tElement, attrs, transclude) {
return function ($scope) {
transclude($scope, function (clone) {
tElement.append(clone);
});
};
}
};
So what's going here? During linking, we are just appending the clone, which is the element we are trying to transclude, into the directive's element. Angular will apply $scope onto the clone element so you can do all the angular goodness inside that element.

To elaborate on #rob's post...
Transclusion requires that Angular creates an element that is a clone of the content of whatever tag the directive is/lives on... If the content is text, it will wrap it in a span.
This is so it has a DOM element to apply the scope to when $compile is called.
So, basically transclude adds an element for the same reason you can't $compile('plain text here {{wee}}').
Now, you can do something sort of like what you're trying to do with $interpolate, which allows you to apply a scope to bindings in a string like "blah {{foo}}".... but since I'm really not sure what you're trying to do, I can't really give you a specific example.

Related

Use $compile inside Angular link function AND access directive arguements

I am making a directive in angular.js 1.x
I call the directive as follows:
<mydirective dirarg={{value-1}}></mydirective>
I would like to create the directive by putting code to manipulate the DOM in the directive's link function. The structure of the DOM generated by the link function is dependant on the value of dirarg, and I would like some elements to have a ng-click attribute.
I have managed to get ng-clicks to work by doing the following:
app.directive('calendar',function($compile){
return{
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>hi</button>")(scope));
}
}
});
When I click the button generated by this directive, the function testt() runs. However, the call to testt() breaks if I try to access dirarg.
app.directive('calendar',function($compile){
return{
scope:{
dirarg: '#'
},
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>"+scope.dirarg+"</button>")(scope));
}
}
});
This code now populates the text of the button with dirarg, but the ng-click functionality breaks. Does somebody know how I can both have ng-click working, and access the arguments to the directive?
To be clear, this button is just an example. My actual situation is a lot more complicated than a button, so don't tell me better ways to make buttons in angular.
When add scope property to directive option it makes isolated scope for directive, you need pass testt function inside directive too, or define testt function inside directive link functiontion
<mydirective dirarg={{value-1}} testt="testt"></mydirective>
app.directive('calendar',function($compile){
return{
scope:{
dirarg: '#',
testt: '='
},
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>"+scope.dirarg+"</button>")(scope));
}
}
});

Angular attribute directive - add an ng-repeat below current directive

If I had an attribute directive, for example something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
where let's say that ngModel and customModel are arrays. Is there a way I can, within the directive's code, add a piece of html below the directives element which could have access to the scope of the directive and be able to reference the customModel so that in the end it looks something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
<div><!-- this code gets added by the custom-directive directive and uses it's scope -->
<span ng-repeat="item in customDirectiveCtrl.customModel" ng-bind="item.property"></span>
</div>
I know I can add html manually using jqLite, however this html doesn't have access to directive scope. The reason I don't want to convert the custom-directive directive from attribute directive to element directive is because it makes it way more difficult to add attributes such as id, name, required, disabled,... to underlying template elements (in the case of this example, a select element)
EDIT: as requested here's an example of how to add an element after the directives element:
{
restrict: 'A',
require: 'ngModel',
scope: { customModel: '=customDirective' },
link: function(scope, element, attrs, ngModel) {
//element.after('<div></div>'); //this adds a div after the directives element
element.after('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>'); //this will add the html in the string, but will not interpret the angular directives within since (i assume) that it is not bound to any scope.
}
}
Any angular component/directive added like this will not work properly or at all.
If you are injecting new HTML into the page in your directive, and you need that HTML to use angular directives (ng-repeat, ng-bind, etc) then you will need to use the $compile service to make angular aware of your new DOM elements. In your case, you would inject the $compile service into your directive and then use it like this:
link: function(scope, element, attrs, ngModel) {
//create the new html
var newElement = angular.element('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>');
//compile it with the scope so angular will execute the directives used
$compile(newElement)(scope); //<-this is the scope in your link function so the "customModel" will be accessible.
//insert the HTML wherever you want it
element.after(newElement);
}

querySelectorAll doesn't work with ngInclude, uiView

I want to fetch all <img> elements inside an element. For this I created a directive 'find-images':
app.directive('findImages', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, element, attrs) {
var img_elements = element[0].querySelectorAll('img');
console.log(img_elements) //doesn't have img elements nested inside ngInclude, uiView
}
}
})
I used this directive in the body tag like this:
<body find-images>
However, I am using ui-router and in many places ng-include(to load nested partials). And the problem I am facing is that querySelectorAll isn't returning img elements which are nested inside ng-include and ui-view. How can I get elements which are nested in them? Also, all nested elements(ui-view, ng-include) are within the body tag.
Implement the logic in the statechangesuccess event fired in ui-router which you could consider the same as page load.
angular.module('app', [])
.run(['$document', function ($document) {
$rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) {
var body = $document.getElementsByTagName("body")[0];
var img_elements = element[body].querySelectorAll('img');
}
});
UPDATE
Another option is to use the onload event raised by ng-include when it is finished rendering.
Also don't forget nginclude is itself a directive, you can use ngswitch in many instances rather than nginclude and render custom directives instead.
That way you have less logic in your controller, and more obvious in the view what is being rendered when.
The problem is, ng-include will include the element after the directive's link function. It will depend on the structure of your app and what you're trying to achieve, but essentially (via timeout, or promises, or callbacks), you need to have a mechanism to determine when your DOM is in a stable state (ng-includes may have more ng-includes etc and it can get complex), and then run your directive logic after that.

How to exclude the directive's own element?

Motivation
Create a layout that directly descends the body element. The layout should wrap the ng-view with a scaffold template.
Constraints
The layout template will have arbitrary content (and potentially any number of root elements, so replace: true will not work here).
What have I tried
Writing a directive that utilizes ng-transclude to wrap the ng-view with the layout structure. As ng-transclude interaction with ng-view seems is no longer supported in version 1.2, no help here.
How, than, can I still exclude the directive's element itself from the DOM?
We can utilize the linking function to replace the directive's target element with the template's contents, as follows:
angular.module('myApp')
.directive('scaffold', function () {
return {
templateUrl: 'views/scaffold-template.html',
restrict: 'EA',
link: function (scope, element, attrs) {
// exclude the directive's own element
element.replaceWith(element.contents());
}
};
});
This comes in handy when the template absolutely must have arbitrary content, or simply can't have one root element.
As this manipulation will take place in all the directive's instances regardless, it's perhaps more appropriate to use the compile function, but link seems sufficient for the proof of concept.
replace property of the Directive Definition Object allows you to specify whether the directive's template will replace the host element ({..., replace: true, ...}) or just insert the template within ({..., replace: false, ...} - default setting).
So in your case you will want to set the replace to true.
One thing to note though is that your directive's template needs to have a single root DOM node, otherwise angular will throw "Error: Template must have exactly one root element". (this is a known limitation).
If your directive's template looks like this:
<br />
<span>{{name}}</span>
you will need to wrap it in single root element, as in:
<span>
<br />
<span>{{name}}</span>
<span>
Note: This is needed only when using replace: true.

Angularjs isolates scope directive with ng-repeat

I'm trying to use directive on ng-repeat items each with an isolate scope but it isn't working. I'm looping through each item and coloring it red with the inboxuser-select directive. However, when I put the directive on, it doesn't show any of my scope values. What is the issue here? Thanks
html file
<li class="inbox-chatter" data-ng-
repeat="inboxuser in inboxusers">
<p inboxuser-select selected={{inboxuser}}">{{inboxuser}}</p>
</li>
directive.js
.directive('inboxuserSelect', function() {
return {
restrict: 'A',
scope: {
selected: "#"
},
link: function(scope, element, attrs) {
scope.selected.css('color','red');
}
}
});
The problem is that once you set an isolate scope on the directive then the whole DOM element has that isolate scope. So the inboxuser from your ng-repeat is no longer in scope when data binding occurs (it's on the parent scope).
One option is to set scope to true instead of using an isolate scope so you'll inherit everything from the parent scope.
Or you can stick with an isolate scope, but pass inboxuser in to the directive and display it using a template. Since you're already passing inboxuser in to the directive's scope through selected it'd be easy to just add this to your directive:
template: '{{selected}}',
Also, by the way, you're missing a quote on your <p>. So this might work better for you (note I also removed {{inboxuser}} from within the <p> assuming you'll be using the template to display that instead):
<p inboxuser-select selected="{{inboxuser}}"></p>
To be honest, I don't understand what you really need to do but I have a feeling that this design will not get you there.
However, I fixed your example just for the purposes of explaining how things work.
You can see it live here.
So... when you write:
scope: {
selected: "#"
}
you are actually saying that my isolated scope will hold a single property named selected which will be of type string and will contain whatever {{inboxuser}} evaluates to. And not only this, whenever inboxuser changes in the outter scope, selected will also change in the inner, isolated scope. This is how '#' binding works.
Whatever you put nested in <p inboxuser-select selected="{{inboxuser}}"></p>, is binded to that isolated scope, which does not have an inboxuser property. So, it has to change to:
<p inboxuser-select selected="{{inboxuser}}">{{selected}}</p>
Finally, scope.selected.css('color','red'); should be changed to:
element.css('color','red');
The element argument in link function is the DOM element where the directive instance is applied. scope.selected is just a string.
I suggest you rething your overall design. If you need help, feel free to ask.
If it helps you, you can use AngScope, a tiny firebug extention i've written. It's just a quick way to inspect $scope instances associated to DOM elements inside firebug's DOM inspector.

Categories

Resources