Inserting a directive from another directive's compile function - angularjs

I want to dynamically insert the <confirmation> element in the DOM from the updater directive. (I have it setup to tap into an event, which it does in my real app) I just need that element inserted and then it'll have the same functionality (as defined in it's respective directive).
Some background: I've tried appending the element and then using the $compile service - $compile(element)(scope), but I think $compile inside a directive's compile function doesn't work. and appending without $compile gives it no angular bindings.
Here's an updated Plnkr: http://plnkr.co/edit/OyBTYGTkMtxryFdDRwQN?p=preview
anyway I can do that? any help would be deeply appreciated even if it's pointing me to the right directon.

You don't need to use the compile property of the Directive Definition Object in the appender directive. You just need the $compile service to compile a new <confirmation> element.
Furthermore, you might want to specify the properties of the isolate scope (i.e. message and/or state):
.directive('appender', function ($compile) {
return {
restrict: 'A',
link: function postLink(scope, elem, attrs) {
elem.after($compile('<confirmation message="..."></confirmation>')(scope));
}
};
});
See, also, this short demo.
UPDATE:
Based on your comments, it is obvious you do not understand the concepts of compiling and linking. Although, you think you are using the compile property, in fact all you need is the linking function. I strongly suggest you take a closer look at the docs regarding the Directive Definition Object.
return {
restrict: 'A',
link: function postLink(scope, element, attrs) {
scope.$watch('items', function(val, oldVal) {
if (val === oldVal) { return; }
element.after($compile('<confirmation message="..."></confirmation>')(scope));
}, true);
}
};
See, also, this other short demo.
UPDATE 2:
Since you are so insistent on compiling from the other directive's compile function,
here is the code:
return {
restrict: 'A',
compile: function () {
var compiled = $compile('<confirmation message="..."></confirmation>');
return function postLink(scope, elem, attrs) {
scope.$watch('items', function(val, oldVal) {
if (val === oldVal) { return; }
elem.after(compiled(scope));
}, true);
};
}
};
and here is the demo.

Related

AngularJS $parser not being called when dynamically adding the directive

So what i'm trying to achieve is to be able to add a directive that contains a parser through another directive.
When directly adding the parser directive on an html element it works completely fine. the parser directive i currently use:
.directive('parseTest', [
function () {
return {
restrict: 'A',
require: ['ngModel'],
link: {
post: function (scope, element, attributes, ctrls) {
var controller = ctrls[0];
controller.$parsers.unshift(function (value) {
var result = value.toLowerCase();
controller.$setViewValue(value);
controller.$render();
return result;
})
}
}
}
}
])
Now when i add this directive through another directive the parser never gets called weirdly enough. The directive that generated the parsetest directive:
.directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
compile: function (elem, attrs) {
elem.attr('parse-test', '');
elem.removeAttr('generate-test');
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])
The following works fine:
<input class="form-control col-sm-6" ng-model="model.parsetest" parse-test/>
The following doesn't work (While the generated result html is the same)
<input class="form-control col-sm-6" ng-model="model.generateTest" generate-test />
So my question is how can i get the parser working when it is in a dynamicly added directive?
Note, i already tried the solution to a similar issue from this question, but that doesn't work for me.
EDIT: Here is a plnkr that demonstrates the issue, both fields have the parse-test directive applied to it that should make the value in the model lowercase, but it only works for the one that is not dynamically added as shown in the console logs.
So I've found the solution, so for anyone that stumbles on the same issue here it is.
The 2 small changes have to made to the directive that generates the directive that contains a parser or formatter.
First of set the priority of the directive to a number higher or equal as 1. Secondly put terminal on true. Those 2 settings seem to resolve the issue.
The problem probably lies in that the default execution of nested directives makes it so that the parser and formatters get inserted slightly to late which is why we need to make sure the directive gets generated first thing before anything else.
This is just an assumption of why it works tho, if anyone else has an explanation it would be great :)
As for the code, the directive that generates another directive should look something like:
directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1,
compile: function (elem, attrs) {
attrs.$set('parseTest', '');
attrs.$set('generateTest', undefined);
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])

Provide template with expressions as attribute on directive

I'm wanting to pass a "template" into a directive, by means of an attribute. Here's a trite example of what I'm trying to accomplish:
This HTML:
<greeter person-name="Jim" greeting-template="Hello {{name}}"></greeter>
Would produce output: Hello Jim.
I've tried with a directive like this:
function greeter($interpolate) {
var directive = {
link: link,
restrict: 'EA',
template: '<div>{{evaluatedTemplate}}</div>'
};
return directive;
function link(scope, element, attrs) {
scope.name = attrs.personName;
scope.evaluatedTemplate = $interpolate(attrs.greetingTemplate)(scope);
}
}
But that doesn't work, because {{name}} in the greeting-template attribute gets evaluated in the parent scope before it gets as far as the directive link function.
Ultimately, I would need the value of attrs.greetingTemplate to literally be a string of: 'Hello {{name}}'. I figure I could do it with some alternative syntax, like having the greeting-template attribute value as: "Hello [name]" and convert "[" to "{{" before interpolation. But that feels messy. I looked at transclusion too, but the way it evaluates the directive against the parent scope looks like it could cause issues when I have multiple greeter's.
Instead of using the link function, you could use the compile function, which runs before any linking to a scope occurs, and gets passed the template element (the original DOM element) as well as its uninterpolated attributes as arguments. I think that's what you're looking for here.
In the compile function, you could store the uninterpolated template string in a variable for later use in your post-link function (which is the same as the link function if you use link rather than compile), where you can then bind it to your scope.
So your directive would look like this, with a compile property rather than a link property:
function greeter($interpolate) {
var directive = {
compile: compile,
restrict: 'EA',
scope: true,
template: '<div>{{evaluatedTemplate}}</div>'
};
return directive;
function compile(tElement, tAttrs) {
// save the uninterpolated template for use in our post-link function
var greetingTemplateUninterpolated = tAttrs.greetingTemplate;
return {
pre: function (scope, element, attrs) {},
post: function (scope, element, attrs) {
scope.name = attrs.personName;
scope.evaluatedTemplate = $interpolate(greetingTemplateUninterpolated)(scope);
}
};
}
}
Here's a fiddle showing it working.
And here's a really good article explaining how compile and link work.

Angular $compile in link function gives an error "RangeError: Maximum call stack size exceeded"

I am trying to add a ng-class attribute to a custom directive from the link function. But when using compile function after adding the ng-class attribute, It throws an error like "RangeError: Maximum call stack size exceeded"
Please see the code below
MyApp.directive('twinField',function($compile){
return {
restrict:'A',
require:'ngModel',
scope:{
fval:'='
},
link:function(scope,ele,attr,ctrl){
ctrl.$validators.compareTo=function(val){
//alert(scope.fval)
return scope.fval==val
}
scope.$watch('fval', function(newValue, oldValue, scope) {
ctrl.$validate()
});
ele.attr("ng-class","addForm.cpassword.$error.compareTo?'errorpswd':''")//=""
$compile(ele)(scope);
}
}
})
It is working when I add the ng-class directly in the html.
$compile(ele)(scope); line in compiling directive element, which will leads to call compile your directive code in infinite loop, that's why it is giving “RangeError: Maximum call stack size exceeded” error.
Ideally you should use combination of compile and link function together. From compile function you need to add ng-class attribute & then remove the directive attribute to avoid directive element to compile indefinitely. Then compile your directive element with scope from directive link function.
Code
myApp.directive('twinField', function($compile) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
fval: '='
},
compile: function(tElement, tAttrs) {
console.log(tElement)
tElement.removeAttr('twin-field');
return function(scope, ele, attr, ctrl) {
ele.attr("ng-class", "addForm.cpassword.$error.compareTo?'errorpswd':''");
ele.attr("test", "{{test}}':''");
var compileFn = $compile(ele);
ctrl.$validators.compareTo = function(val) {
//alert(scope.fval)
return scope.fval == val
}
scope.$watch('fval', function(newValue, oldValue, scope) {
ctrl.$validate()
});
compileFn(scope);
}
}
}
})
Similar answer
Demo here
But the other thing, I don't see any advantage with your code of adding and removing class using ng-class directive. As you are setting validity of your form control, implicitly you are adding and removing ng-valid-compare-to(on valid) & ng-invalid-compare-to(on invalid) class. So there is no need to create extra overhead to have ng-class logic to putting same thing again.

Angular composite (super) directive not allowing 2 way binding on component (child) directives

I have a need to create a composite directive that incorporates separate fully functional directives. One of my component directives adds an element to the dom and that element binds to a value in the component directive's controller. When the composite directive adds the component directive in the compile function, it seems to work but the piece that has the 2 way binding in the component directive does not appear to get compiled and just renders the {{ctrl.value}} string on the screen. I realize this is a bit convoluted so I have included a plunk to help clarify the issue.
app.directive('compositeDirective', function($compile){
return {
compile: compileFunction
}
function compileFunction(element, attrs){
attrs.$set("component-directive", "");
element.removeAttr("composite-directive");
element.after("<div>Component value when added in composite directive: {{compCtrl.myValue}}</div>");
return { post: function(scope, element){
$compile(element)(scope);
}};
}
});
app.directive('componentDirective', function(){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
element.after("<div>Component value: {{compCtrl.myValue}}</div>");
}
});
app.controller('componentController', function(){
var vm = this;
vm.myValue = "Hello";
});
http://plnkr.co/edit/alO83j9Efz62VTKDOVgc
I don't think any compilation will happen as a result of changes in the link function, unless you call $compile manually, i.e.,
app.directive('componentDirective', function($compile){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
var elm = $compile("<div>Component value: {{compCtrl.myValue}}</div>")(scope);
element.append(elm);
}
});
Updated plunk: http://plnkr.co/edit/pIixQujs1y6mPMKT4zxK
You can also use a compile function instead of link: http://plnkr.co/edit/fjZMd4FIQ97oHSvetOgU
Also, make sure to use .append() instead of .after().

angular ng-bind-html and directive within it

Plunker Link
I have a element which I would like to bind html to it.
<div ng-bind-html="details" upper></div>
That works. Now, along with it I also have a directive which is bound to the bound html:
$scope.details = 'Success! <a href="#/details/12" upper>details</a>'
But the directive upper with the div and anchor do not evaluate. How do I make it work?
I was also facing this problem and after hours searching the internet I read #Chandermani's comment, which proved to be the solution.
You need to call a 'compile' directive with this pattern:
HTML:
<div compile="details"></div>
JS:
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}])
You can see a working fiddle of it here
Thanks for the great answer vkammerer. One optimization I would recommend is un-watching after the compilation runs once. The $eval within the watch expression could have performance implications.
angular.module('vkApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
}
);
};
}]);
Here's a forked and updated fiddle.
Add this directive angular-bind-html-compile
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
// Incase value is a TrustedValueHolderType, sometimes it
// needs to be explicitly called into a string in order to
// get the HTML string.
element.html(value && value.toString());
// If scope is provided use it, otherwise use parent scope
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
Use it like this :
<div bind-html-compile="data.content"></div>
Really easy :)
Unfortunately I don't have enough reputation to comment.
I could not get this to work for ages. I modified my ng-bind-html code to use this custom directive, but I failed to remove the $scope.html = $sce.trustAsHtml($scope.html) that was required for ng-bind-html to work. As soon as I removed this, the compile function started to work.
For anyone dealing with content that has already been run through $sce.trustAsHtml here is what I had to do differently
function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(function(scope) {
return $sce.parseAsHtml(attrs.compile)(scope);
},
function(value) {
// when the parsed expression changes assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current scope.
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
});
}
This is only the link portion of the directive as I'm using a different layout. You will need to inject the $sce service as well as $compile.
Best solution what I've found! I copied it and it work's exactly as I needed. Thanks, thanks, thanks ...
in directive link function I have
app.directive('element',function($compile){
.
.
var addXml = function(){
var el = $compile('<xml-definitions definitions="definitions" />')($scope);
$scope.renderingElement = el.html();
}
.
.
and in directive template:
<span compile="renderingElement"></span>

Resources