How to finish compiling element, if directive sets terminal: true? - angularjs

Code is here.
I'm trying to create a directive that re-arranges it's child elements. I can't use a simple ng-transclude because I want to put some child elements in different places within the template. I've learned that I need to set terminal: true and control compilation myself, but how do you do that? As you can see in that code the ng-if and ng-model on the child elements have been compiled, but are not working properly.
One specific thing I may be doing wrong: the second argument to the $compile function. I don't know what it is, and the documentation, says nothing but "function available to directives".
Here's the directive in question:
.directive('controlGroup', function ($compile, $log) {
var template = "<div class='control-group'>" +
"<label class='control-label'></label>" +
"<div class='controls'></div>" +
"</div>";
return {
restrict: 'A',
terminal: true,
priority: 100,
compile: function (elt, attrs) {
// Re-arrange element, inserting parts into template from above.
var labelText = elt.find('label').text();
var inputsAndMessages = elt.children().filter('input, button, select, .text-error');
var newElt = $(template);
newElt.find('.control-label').text(labelText);
newElt.find('.controls').append(inputsAndMessages);
elt.html('').append(newElt);
// Now, how to finish compiling and linking it? What to pass for 2nd arg?
var link_ = $compile(elt, null, 99);
function link (scope, elt, attrs) {
}
return link;
}
};
})

This line var link_ = $compile(elt, null, 99); returned a template function. $compile docs :
Compiles a piece of HTML string or DOM into a template and produces a
template function, which can then be used to link scope and the
template together.
Now you just need to execute that template against a scope. Since there's no scope at compile time, we need to do it in your link function, like so:
function link (scope, elt, attrs) {
link_(scope);
}
That fixes it: Working plunker
The second parameter is a transclusion function that would give you access to the cloned element and scope if you were transcluding. Since you're not transcluding, null is fine to pass in.

Related

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 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().

Resolving a directive inside a linking function

I'm trying to build a mechanism which will allow me to resolve directives within a linking function.
Example:
angular.module('directives', [])
.directive('myContainer', ['$compile', function ($compile) {
return {
restrict: "E",
replace: true,
link: function (scope, element, attrs) {
angular.forEach(scope.component.components, function(component){
var newScope = scope.$new()
newScope.component = component;
var elem = angular.element('<'+component.type+'>'+'</'+component.type+'>')
//Trying to compile directive to be resolved
var resolvedDirective = $compile(elem)(scope)
element.append(resolvedDirective)
})
}
});
Problem is, the "resolvedDirective" (defined by component => type) simply creates a tag containing the other directive name, which will be resolved later on.
My mechanism was simplified for the sake of the example (recursive...)
Hope I've made my question clear enough...
Thanks in advance!
Need more clarity in your Question . Please update with desired result of your directive.
You can follow this pattern. This answer has explained lot about how to add compile directives within directives
Angularjs directive add directives as attribute and bind them dynamically
In your case you want to append new directive , compile and bind it .
So try to add append your element in Compile phase and use $compile in postLink phase.

angular directive with isolate scope, fields inaccessible

I'm trying to write a directive which takes a scope variable name and assigns to it the result of passing a different named parameter into a function. Below, the files="result" is intended to create a {{result}} variable in the glob isolate scope. The contents of the "matching" variable are to be evaluated in the parent context, and assigned to an isolate 'matching' variable.
the directive then calls a function eventually assigning to the isolate variable pointed to by files (result here) the array returned. expansion of {{result}} could then be used for example in an ng-repeat.
The directive should be reusable without changing the variable names.
This isn't happening. If I assign everything to a parent, I can get it working but need to change the variable names each time.
angular.module('j20-glob', ['api'])
/*
* usage: <glob files="result" matching="/bin/{{prefix}}*">
* {{result}}
* </glob>
* should allow another just after the first without stomping result
* <glob files="result" matching="/something">{{result}}</glob>
*/
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
link: function(scope, iElement, iAttributes) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
});
}
}
}
]);
If I understand your code here, you are having a similar issue to what I answered here: Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27.
You have created an Element Directive, called glob. The Directive has an Isolate Scope, which you attach a property, result in your example. This all works fine. The problem is, the property in the isolate scope is only accessible within the directive; and in your case, you are trying to access it outside the directive.
The Element <glob></glob> is your directive. This Element can be containers for other Elements, for example an angular expression {{result}} but these Elements are not part of the Directive, and therefore not scoped in the isolate.
If you were to include a template, and place {{result}} inside the template, you would see the expected result. However, this stops working if you change the variable you are passing in.
A rough draft of a working Directive using a transclude function might be something like:
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
transclude : true,
link: function(scope, iElement, iAttributes, ctrl, transclude) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
//append our scope into the DOM element (clone) instead of $scope
transclude(scope, function(clone, scope){
element.append(clone);
});
});
}
}
}
]);

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
++

Resources