Directive 'recompilation' into another - angularjs

I'm wondering if there is an easy way to accomplish this kind of attribute directive:
<p my-attr-directive="ABC">go to ABC</p>
should be recompiled in a ui-router parameterized link:
<p ui-sref="param_state({id:'ABC'})">go to ABC</p>
The state param_state is fixed, the new directive should defines the parameter id.
EDIT: I almost done, I'm not sure why $compile, doesn't compile
LINK!!
app.directive('myAttrDirective', function($compile) {
return {
restrict: 'A',
link: function($scope, el, $attrs) {
$attrs.$set("uiSref","param_state({id:'"+$attrs.myAttrDirective+"'})");
$compile(el.contents())($scope);
}
};
});

I almost done, I'm not sure why $compile, doesn't compile
If you inspected the anchor element, you'd see something like the following in it,
<span class="ng-scope">LINK!</span>
which means $compile has done its job. In this regard, just binding the contents of the element (i.e. the textNode 'LINK!') to the $scope.
Therefore, $compile is compiling, but it hasn't found your dynamically added directive uiSref anywhere searching.
$compile(el.contents())($scope); // <== your directive exists as an attribute of `el` itself
Solution
Try to compile the host element itself since you add the uiSref directive there as an attribute. Also, its important to remove the my-attr-directive attribute from the element or else you'll end up infinitely compiling the anchor element:
link: function($scope, el, $attrs) {
// make sure you don't recursively compile
el.removeAttr('my-attr-directive');
$attrs.$set("uiSref","param_state({id:'"+$attrs.myAttrDirective+"'})");
$compile(el)($scope); // <== here
}
Working Demo

Related

AngularJS directive : require ngBind

I'm currently writing an attribute directive relying on the use of ngBind. I need the element to bear a ngBind attribute for the directive to work. I was thinking that a simple require: 'ngBind' would be enough, just like you'd do with ngModel. So here's what I did :
app.directive( 'myDirective', function() {
return {
restrict: 'A',
require: 'ngBind',
link: function(scope, element, attrs) { .. }
});
And here's how I use my directive:
<span my_directive="" ng_bind="valueToBeBound"></span>
But then I get this error, so I suppose it can't be done this way:
Error: error:ctreq
Missing Required Controller
Controller 'ngBind', required by directive 'myDirective', can't be found!
Is there any means to force the presence of ngBind ?
Thanks !
This is an expected behaviour. As defined in the AngularJS documentation for a directive's require option states that:
Require another directive and inject its controller as the fourth
argument to the linking function. The require takes a string name (or
array of strings) of the directive(s) to pass in. If an array is used,
the injected argument will be an array in corresponding order. If no
such directive can be found, or if the directive does not have a
controller, then an error is raised (unless no link function is
specified, in which case error checking is skipped).
Since the ngBind directive required by myDirective does not have a controller then an error is expected to be raised, unless you remove the link function in your myDirective directive then angular will simply skip the error checking.
There are two ways to achieve what you want.
Remove the link() function in your myDirective directive then add a controller function in that directive to add your component logic. The problem with this solution is that you can't attach DOM logic in your link() function.
The most ideal way to deal with the problem is to simply remove the require option and simply check the existence of the ngBind attribute in the element where your myDirective directive resides.
e.g.
app.directive( 'myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(angular.isUndefined(attrs.ngBind)) {
return;
}
// Your myDirective DOM LOGIC/MANIPULATION should be here
}
});
As explained well here: Angular NgModelController you need to provide the ngBind
<span ng-bind="ModelName"></span>

wrapping inputs in directives in angular

I had the idea to wrap inputs into custom directives to guarantee a consistent look and behavior through out my site. I also want to wrap bootstrap ui's datepicker and dropdown. Also, the directive should handle validation and display tooltips.
The HTML should look something like this:
<my-input required max-length='5' model='text' placeholder='text' name='text'/>
or
<my-datepicker required model='start' placeholder='start' name='start'/>
in the directives i want to create a dom structure like:
<div>
<div>..</div> //display validation in here
<div>..</div> //add button to toggle datepicker (or other stuff) in here
<div>..</div> //add input field in here
</div>
I tried various ways to achieve this but always came across some tradeoffs:
using transclude and replace to insert the input into the directives dom structure (in this case the directive would be restricted to 'A' not 'E' like in the example above). The problem here is, that there is no easy way to access the transcluded element as I want to add custom attributes in case of datepicker. I could use the transclude function and then recompile the template in the link function, but this seems a bit complex for this task. This also leads to problems with the transcluded scope and the toggle state for the datepicker (one is in the directives scope, the other in the transcluded scope).
using replace only. In this case, all attributes are applied to the outermost div (even if I generate the template dom structure in the compile function). If I use just the input as template, then the attributes are on the input, but I need to generate the template in the link function an then recompile it. As far as I understand the phase model of angular, I would like to avoid recompiling and changing the template dom in the link function (although I've seen many people doing this).
Currently I'm working with the second approach and generating the template in the link function, but I was wondering if someone had some better ideas!
Here's what I believe is the proper way to do this. Like the OP I wanted to be able to use an attribute directive to wrapper an input. But I also wanted it to work with ng-if and such without leaking any elements. As #jantimon pointed out, if you don't cleanup your wrapper elements they will linger after ng-if destroys the original element.
app.directive("checkboxWrapper", [function() {
return {
restrict: "A",
link: function(scope, element, attrs, ctrl, transclude) {
var wrapper = angular.element('<div class="wrapper">This input is wrappered</div>');
element.after(wrapper);
wrapper.prepend(element);
scope.$on("$destroy", function() {
wrapper.after(element);
wrapper.remove();
});
}
};
}
]);
And here's a plunker you can play with.
IMPORTANT: scope vs element $destroy. You must put your cleanup in scope.$on("$destroy") and not in element.on("$destroy") (which is what I was originally attempting). If you do it in the latter (element) then an "ngIf end" comment tag will get leaked. This is due to how Angular's ngIf goes about cleaning up its end comment tag when it does its falsey logic. By putting your directive's cleanup code in the scope $destroy you can put the DOM back like it was before you wrappered the input and so ng-if's cleanup code is happy. By the time element.on("$destroy") is called, it is too late in the ng-if falsey flow to unwrap the original element without causing a comment tag leak.
Why not doing a directive like that?
myApp.directive('wrapForm', function(){
return {
restrict: 'AC',
link: function(scope, inputElement, attributes){
var overallWrap = angular.element('<div />');
var validation = angular.element('<div />').appendTo(overallWrap);
var button = angular.element('<div />').appendTo(overallWrap);
var inputWrap = angular.element('<div />').appendTo(overallWrap);
overallWrap.insertBefore(inputElement);
inputElement.appendTo(inputWrap);
inputElement.on('keyup', function(){
if (inputElement.val()) {
validation.text('Just empty fields are valid!');
} else {
validation.text('');
}
});
}
}
});
Fiddle: http://jsfiddle.net/bZ6WL/
Basically you take the original input field (which is, by the way, also an angularjs directive) and build the wrappings seperately. In this example I simply build the DIVs manually. For more complex stuff, you could also use a template which get $compile(d) by angularjs.
The advantage using this class or html attribute "wrapForm": You may use the same directive for several form input types.
Why not wrap the input in the compile function?
The advantage is that you will not have to copy attributes and will not have to cleanup in the scope destroy function.
Notice that you have to remove the directive attribute though to prevent circular execution.
(http://jsfiddle.net/oscott9/8er3fu0r/)
angular.module('directives').directive('wrappedWithDiv', [
function() {
var definition = {
restrict: 'A',
compile: function(element, attrs) {
element.removeAttr("wrapped-with-div");
element.replaceWith("<div style='border:2px solid blue'>" +
element[0].outerHTML + "</div>")
}
}
return definition;
}
]);
Based on this: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/
This directive does transclusion, but the transcluded stuff uses the parent scope, so all bindings work as if the transcluded content was in the original scope where the wrapper is used. This of course includes ng-model, also min/max and other validation directives/attributes. Should work for any content. I'm not using the ng-transclude directive because I'm manually cloning the elements and supplying the parent(controller's) scope to them. "my-transclude" is used instead of ng-transclude to specify where to insert the transcluded content.
Too bad ng-transclude does not have a setting to control the scoping. It would make all this clunkyness unnecessary.
And it looks like they won't fix it: https://github.com/angular/angular.js/issues/5489
controlsModule.directive('myWrapper', function () {
return {
restrict: 'E',
transclude: true,
scope: {
label: '#',
labelClass: '#',
hint: '#'
},
link: link,
template:
'<div class="form-group" title="{{hint}}"> \
<label class="{{labelClass}} control-label">{{label}}</label> \
<my-transclude></my-transclude> \
</div>'
};
function link(scope, iElement, iAttrs, ctrl, transclude) {
transclude(scope.$parent,
function (clone, scope) {
iElement.find("my-transclude").replaceWith(clone);
scope.$on("$destroy", function () {
clone.remove();
});
});
}
});

Manipulating DOM in directive, now what?

I have made a directive (inline-edit) and manipulated the DOM in the compile function, but how can I make the other directives that I have added to work? I guess I need to compile it, but how? See my jsfiddle here: http://jsfiddle.net/tidelipop/m4gbZ/
ng-click does not work as it is, but the strange thing is, why do ng-bind work? You can see that it does work if you unhide the textarea in dev tools.
angular.module('MyApp', [], function($compileProvider){
$compileProvider.directive("inlineEdit", function($compile, $q){
return {
restrict: "A",
scope: true,
controller: function($scope){
$scope.editMode = true;
$scope.save = function(){
console.log("Saving...");
};
},
compile: function(tElement, tAttrs){
tElement
.attr("ng-hide", "editMode")
.attr("ng-click", "editMode=!editMode")
.after("<textarea ng-show=\"editMode\" ng-model=\""+tAttrs.ngBind+"\"></textarea><button ng-click=\"save()\">Save</button>");
//var scopeResolver = $q.defer();
//$compile(tElement.parent().contents())(scopeResolver.promise);
return function(scope, element, attrs, controller){
//scopeResolver.resolve(scope);
//$compile(element.parent().contents())(scope);
console.log(element.parent().contents());
};
}
};
});
})
.controller("UserAdminCtrl", function($scope){
$scope.data_copy = {
user: {
user_id: 'sevaxahe',
comment: 'test'
}
};
});
It looks like your directive is conflicting with the ng-bind, I don't really know why, but the question I asked myself looking at your code was : Wouldn't it be easier using a template and a custon attribute for the model (instead of ng-bind) ?
And the answer is yes !
Actually that's just my opinion, but here is what I did by modifying your code http://jsfiddle.net/DotDotDot/m4gbZ/73/
I let you have a look, I had to change some parts (the ng-click doesn't work well on the textarea so I put this behavior on the Save button) but I think this is almost what you wanted. On code side, I modified the HTML to avoid calling ng-bind, using a custom scope variable which will be caught in the directive :
<span inline-edit ff="data_copy.user.comment">First</span>
On the directive side, I got rid of all the compile/controller stuff, and I added a template
return {
restrict: "A",
template:'<div><span ng-hide="editMode" ng-click="editMode=!editMode">{{aModel}}</span><textarea ng-show="editMode" ng-model="aModel"></textarea> <button ng-click="save()">{{getLabel()}}</button></div>',
replace:true,
scope: {aModel:'=ff'},
link: function(scope, element, attrs){
console.log(element)
scope.editMode = true;
scope.save = function(){
console.log("Saving...");
scope.editMode=!scope.editMode;
};
scope.getLabel=function(){
if(scope.editMode)
return "Save";
else
return "Change";
}
console.log(element.parent().contents());
}
}
Why ? The template, because angular will compile it itself without any intervention.
I added replace:true to replace the line, but it's optionnal
The scope part is more important. scope: {'=ff'} tells angular that I want to use an isolated scope, and I want the scope.aModel value to be bound with the ff variable passed in the HTML.
The '=' means that the modifications will be evaluated from the parent scope, and every modification will be reflected in the parent and in the directive
I replaced your controller and your compile function (no element to compile, and adding function can be done here instead of a dedicated controller) by a linking function containing the functions needed. As I said before, I added the editMode change behavior to the Save button, so I added a bit more code, but it's not the main point, I think you may have to change things here to reflect your expected behavior
I hope this will help you, as I don't really answer your question, but I think you could also explore this way
++

Avoid using extra DOM nodes when using nginclude

I'm struggling to wrap my mind around how to have an ng-include not use an extra DOM element as I'm building an angular app from a plain-HTML demo. I'm working with pretty slim HTML with fully developed, tightly DOM-coupled CSS (built from SASS) and refactoring is something I want to avoid at all costs.
Here's the actual code:
<div id="wrapper">
<header
ng-controller="HeaderController"
data-ng-class="headerType"
data-ng-include="'/templates/base/header.html'">
</header>
<section
ng-controller="SubheaderController"
data-ng-class="subheaderClass"
ng-repeat="subheader in subheaders"
data-ng-include="'/templates/base/subheader.html'">
</section>
<div
class="main"
data-ng-class="mainClass"
data-ng-view>
</div>
</div>
I need <section> to be a repeating element but have its own logic and different content. Both, content and number of repetitions are dependent on business logic. As you can see, putting the ng-controller and the ng-repeat on the <section> element will not work. What would, however, is to insert a new DOM node, which is what I'm trying to avoid.
What am I missing out? Is this best practice or is there a better way?
EDIT: just to clarify as asked in comments, the final HTML I'm trying to generate would be:
<div id="wrapper">
<header>...</header>
<section class="submenuX">
some content from controller A and template B (e.g. <ul>...</ul>)
</section>
<section class="submenuY">
different content from same controller A and template B (e.g. <div>...</div>)
</section>
<section class="submenuZ">
... (number of repetitions is defined in controller A e.g. through some service)
</section>
<div>...</div>
</div>
The reason I want to use the same template B (subheader.html), is for code cleanliness. I conceive subheader.html to have some kind of ng-switch in order to return dynamic content.
But basically, the underlaying quiestion is: is there a way to include the contents of a template transparently, without using a DOM node?
EDIT2: The solution needs to be reusable. =)
Some of the other answers suggest replace:true, but keep in mind that replace:true in templates is marked for deprecation.
Instead, in an answer to a similar question, we find an alternative: It allows you to write:
<div ng-include src="dynamicTemplatePath" include-replace></div>
Custom Directive:
app.directive('includeReplace', function () {
return {
require: 'ngInclude',
restrict: 'A', /* optional */
link: function (scope, el, attrs) {
el.replaceWith(el.children());
}
};
});
(cut'n'paste from the other answer)
Edit: After some research and for the sake of completeness, I've added some info. Since 1.1.4, the following works:
app.directive('include',
function () {
return {
replace: true,
restrict: 'A',
templateUrl: function (element, attr) {
return attr.pfInclude;
}
};
}
);
Usage:
<div include="'path/to/my/template.html'"></div>
There is, however, one gotcha: the template cannot be dynamic (as in, passing a variable through scope because $scope, or any DI for that matter, is not accessible in templateUrl - see this issue), only a string can be passed (just like the html snippet above). To bypass that particular issue, this piece of code should do the trick (kudos to this plunker):
app.directive("include", function ($http, $templateCache, $compile) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
var templateUrl = scope.$eval(attributes.include);
$http.get(templateUrl, {cache: $templateCache}).success(
function (tplContent) {
element.replaceWith($compile(tplContent.data)(scope));
}
);
}
};
});
Usage:
<div include="myTplVariable"></div>
You can create a custom directive, linking to the template with the templateUrl property, and setting replace to true:
app.directive('myDirective', function() {
return {
templateUrl: 'url/to/template',
replace: true,
link: function(scope, elem, attrs) {
}
}
});
That would include the template as-is, without any wrapper element, without any wrapper scope.
For anyone who happens to visit this question:
As of angular 1.1.4+ you can use a function in the templateURL to make it dynamic.
Check out this other answer here
With the right setup, you can define your own ngInclude directive that can run instead of the one provided by Angular.js and prevent the built-in directive to execute ever.
To prevent the Angular-built-in directive from executing is crucial to set the priority of your directive higher than that of the built-in directive (400 for ngInclude and set the terminal property to true.
After that, you need to provide a post-link function that fetches the template and replaces the element's DOM node with the compiled template HTML.
A word of warning: this is rather draconian, you redefine the behavior of ngInclude for your whole application. I therefore set the directive below not on myApp but inside one of my own directives to limit its scope. If you want to use it application-wide, you might want to make its behavior configurable, e.g. only replace the element if a replace attribute is set in the HTML and per default fall back to setting innerHtml.
Also: this might not play well with animations. The code for the original ngInclude-directive is way longer, so if you use animations in your application, c&p the original code and shoehorn the `$element.replaceWith() into that.
var includeDirective = ['$http', '$templateCache', '$sce', '$compile',
function($http, $templateCache, $sce, $compile) {
return {
restrict: 'ECA',
priority: 600,
terminal: true,
link: function(scope, $element, $attr) {
scope.$watch($sce.parseAsResourceUrl($attr.src), function ngIncludeWatchAction(src) {
if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) {
var e =$compile(response)(scope);
$element.replaceWith(e);
});
}
});
}
};
}];
myApp.directive('ngInclude', includeDirective);

Link vs compile vs controller

When you create a directive, you can put code into the compiler, the link function or the controller.
In the docs, they explain that:
compile and link function are used in different phases of the angular
cycle
controllers are shared between directives
However, for me it is not clear, which kind of code should go where.
E.g.: Can I create functions in compile and have them attached to the scope in link or only attach functions to the scope in the controller?
How are controllers shared between directives, if each directive can have its own controller? Are the controllers really shared or is it just the scope properties?
Compile :
This is the phase where Angular actually compiles your directive. This compile function is called just once for each references to the given directive. For example, say you are using the ng-repeat directive. ng-repeat will have to look up the element it is attached to, extract the html fragment that it is attached to and create a template function.
If you have used HandleBars, underscore templates or equivalent, its like compiling their templates to extract out a template function. To this template function you pass data and the return value of that function is the html with the data in the right places.
The compilation phase is that step in Angular which returns the template function. This template function in angular is called the linking function.
Linking phase :
The linking phase is where you attach the data ( $scope ) to the linking function and it should return you the linked html. Since the directive also specifies where this html goes or what it changes, it is already good to go. This is the function where you want to make changes to the linked html, i.e the html that already has the data attached to it. In angular if you write code in the linking function its generally the post-link function (by default). It is kind of a callback that gets called after the linking function has linked the data with the template.
Controller :
The controller is a place where you put in some directive specific logic. This logic can go into the linking function as well, but then you would have to put that logic on the scope to make it "shareable". The problem with that is that you would then be corrupting the scope with your directives stuff which is not really something that is expected.
So what is the alternative if two Directives want to talk to each other / co-operate with each other? Ofcourse you could put all that logic into a service and then make both these directives depend on that service but that just brings in one more dependency. The alternative is to provide a Controller for this scope ( usually isolate scope ? ) and then this controller is injected into another directive when that directive "requires" the other one. See tabs and panes on the first page of angularjs.org for an example.
I wanted to add also what the O'Reily AngularJS book by the Google Team has to say:
Controller - Create a controller which publishes an API for communicating across directives. A good example is Directive to Directive Communication
Link - Programmatically modify resulting DOM element instances, add event listeners, and set up data binding.
Compile - Programmatically modify the DOM template for features across copies of a directive, as when used in ng-repeat. Your compile function can also return link functions to modify the resulting element instances.
A directive allows you to extend the HTML vocabulary in a declarative fashion for building web components. The ng-app attribute is a directive, so is ng-controller and all of the ng- prefixed attributes. Directives can be attributes, tags or even class names, comments.
How directives are born (compilation and instantiation)
Compile: We’ll use the compile function to both manipulate the DOM before it’s rendered and return a link function (that will handle the linking for us). This also is the place to put any methods that need to be shared around with all of the instances of this directive.
link: We’ll use the link function to register all listeners on a specific DOM element (that’s cloned from the template) and set up our bindings to the page.
If set in the compile() function they would only have been set once (which is often what you want). If set in the link() function they would be set every time the HTML element is bound to data in the
object.
<div ng-repeat="i in [0,1,2]">
<simple>
<div>Inner content</div>
</simple>
</div>
app.directive("simple", function(){
return {
restrict: "EA",
transclude:true,
template:"<div>{{label}}<div ng-transclude></div></div>",
compile: function(element, attributes){
return {
pre: function(scope, element, attributes, controller, transcludeFn){
},
post: function(scope, element, attributes, controller, transcludeFn){
}
}
},
controller: function($scope){
}
};
});
Compile function returns the pre and post link function. In the pre link function we have the instance template and also the scope from the controller, but yet the template is not bound to scope and still don't have transcluded content.
Post link function is where post link is the last function to execute. Now the transclusion is complete, the template is linked to a scope, and the view will update with data bound values after the next digest cycle. The link option is just a shortcut to setting up a post-link function.
controller: The directive controller can be passed to another directive linking/compiling phase. It can be injected into other directices as a mean to use in inter-directive communication.
You have to specify the name of the directive to be required – It should be bound to same element or its parent. The name can be prefixed with:
? – Will not raise any error if a mentioned directive does not exist.
^ – Will look for the directive on parent elements, if not available on the same element.
Use square bracket [‘directive1′, ‘directive2′, ‘directive3′] to require multiple directives controller.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $element) {
});
app.directive('parentDirective', function() {
return {
restrict: 'E',
template: '<child-directive></child-directive>',
controller: function($scope, $element){
this.variable = "Hi Vinothbabu"
}
}
});
app.directive('childDirective', function() {
return {
restrict: 'E',
template: '<h1>I am child</h1>',
replace: true,
require: '^parentDirective',
link: function($scope, $element, attr, parentDirectCtrl){
//you now have access to parentDirectCtrl.variable
}
}
});
Also, a good reason to use a controller vs. link function (since they both have access to the scope, element, and attrs) is because you can pass in any available service or dependency into a controller (and in any order), whereas you cannot do that with the link function. Notice the different signatures:
controller: function($scope, $exceptionHandler, $attr, $element, $parse, $myOtherService, someCrazyDependency) {...
vs.
link: function(scope, element, attrs) {... //no services allowed
this is a good sample for understand directive phases
http://codepen.io/anon/pen/oXMdBQ?editors=101
var app = angular.module('myapp', [])
app.directive('slngStylePrelink', function() {
return {
scope: {
drctvName: '#'
},
controller: function($scope) {
console.log('controller for ', $scope.drctvName);
},
compile: function(element, attr) {
console.log("compile for ", attr.name)
return {
post: function($scope, element, attr) {
console.log('post link for ', attr.name)
},
pre: function($scope, element, attr) {
$scope.element = element;
console.log('pre link for ', attr.name)
// from angular.js 1.4.1
function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) {
element.css(style, '');
});
}
if (newStyles) element.css(newStyles);
}
$scope.$watch(attr.slngStylePrelink, ngStyleWatchAction, true);
// Run immediately, because the watcher's first run is async
ngStyleWatchAction($scope.$eval(attr.slngStylePrelink));
}
};
}
};
});
html
<body ng-app="myapp">
<div slng-style-prelink="{height:'500px'}" drctv-name='parent' style="border:1px solid" name="parent">
<div slng-style-prelink="{height:'50%'}" drctv-name='child' style="border:1px solid red" name='child'>
</div>
</div>
</body>
compile: used when we need to modify directive template, like add new expression, append another directive inside this directive
controller: used when we need to share/reuse $scope data
link: it is a function which used when we need to attach event handler or to manipulate DOM.

Resources