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.
Related
In the following snippet, I want to ask that if compile phase runs only once for all the instances of a directive, then why am I seeing console.log("compile") run 5 times? It should have run only once? Isn't it?
//module declaration
var app = angular.module('myApp',[]);
//controller declaration
app.controller('myCtrl',function($scope){
$scope.name = "Joseph";
});
//app declaration
app.directive('myStudent',function(){
return{
template: "Hi! Dear!! {{name}}<br/>",
compile: function(elem, attr){
console.log("compile");
}
}
});
<body ng-app="myApp" ng-controller="myCtrl">
<my-student></my-student>
<my-student></my-student>
<my-student></my-student>
<my-student></my-student>
<my-student></my-student>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular.min.js"></script>
</body>
HTML compilation happens in three phases:
$compile traverses the DOM and matches directives.
If the compiler finds that an element matches a directive, then the directive is added to the list of directives that match the DOM element. A single element may match multiple directives.
Once all directives matching a DOM element have been identified, the compiler sorts the directives by their priority.
Each directive's compile functions are executed. Each compile function has a chance to modify the DOM. Each compile function returns a link function. These functions are composed into a "combined" link function, which invokes each directive's returned link function.
$compile links the template with the scope by calling the combined linking function from the previous step. This in turn will call the linking function of the individual directives, registering listeners on the elements and setting up $watchs with the scope as each directive is configured to do.
From Angular documentation "What are Directives?
At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children."
So when angular compiles the html, it traverse through the mark up and whenever it finds <my-student></my-student> mark up it will try attaching "the specific behaviour". To get the specific behavior it need to compile the directive each time since each instance of directives might have different attributes or parameters.
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);
}
Plunker:
Direct Edit example.
In the above plunker, I have a directive (direct-edit) which uses
transclude: 'element',
to surround the element with other markup. The directive
requires: 'ngModel',
to connect the additional markup to the model. However, because the initial element is wrapped up with ng-transclude, it becomes disconnected from the model. Does anyone know how to fix this?
EDIT (from comments below):
To clarify: I want to take any arbitrary directive:
<custom attr1="" attr2="" style="" ng-model="random" />
and add the direct-edit directive so that the arbitrary directive is paired with a field that edits the value directly. For the purposes of simplicity, I'm only showing a text input in the example code.
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.
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.