How to wrap an existing DOM element with a directive in AngularJS - angularjs

I have an existing DOM element that has already been compiled and contains angular directives, for example
CurrentDirective
function link(scope, element, attr, ctrls) {
var element = element.children()[1]; //Ex HTML: <div testElement>test</div> )
// Wrap element now with WrapperDirective
Now I want to wrap it with a new directive called "WrapperDirective" .
The catch is, "WrapperDirective" needs it's "element" parameter at link time, to run this code:
WrapperDirective
link: function (scope, element, attrs) {
var testElements = angular.element(element[0].querySelectorAll('[testElement]'));
The result should be similar to having this code, but it has to be dynamically created using the already compiled inner piece.
<WrapperDirective>
<div testElement>test</div> // Just example, could be anything.
</WrapperDirective>
Basically I need something like
$compile("<WrapperDirective>" + Existing DOM Element(No Recompile)+ "</WrapperDirective")
I know I can
$compile("<WrapperDirective></WrapperDirective>")
and then append the rest, but the link function of WrapperDirective will not get the children element that it needs.

Related

Add elements to datatable

I'm using angular-datatables and wondering how could I add various elements such as buttons, text inputs etc to already rendered and set datatable. I think it may be possible through new angular directive and link function, e.g.:
link = (scope, element, attrs) =>
$('#table_wrapper div.top').append(some nice custom element)
this directive is taking place in the parent div:
<div my-custom-directive>
<table id="table" datatable="ng" ... etc
The problem is datatable building elements such as 'table_wrapper' and others are not ready yet when I try to add my new custom elements to them.
One way as I see to handle that problem is:
angular-datatables has a directive called dt-instance:
< table id="table" datatable="ng" dt-instance="yourCallback",
where callback is a function wherein among other useful things you can set a control flag in the scope, so in my controller i have:
yourCallback: (dtInstance) =>
$scope.flag= 1
next in my-custom-directive I watch this flag:
link = (scope, element, attrs) =>
scope.$watch('flag', (newval, oldval) =>
if (newval)
$('#table_wrapper div.top').append(some nice custom element)

Directive custom control

I am trying to create a directive as custom video control. I loading an html file to templateUrl of this directive. The problem is when there are more than one controls, they have the same src file set to all of them and they are sharing state of video as well. When I pause from another control, it pauses video being played on 1st control. Here is directive template that I am using:
dApp.directive('myVideoControl', function(){
return {
scope: {
cameraUrl: '=vcCameraUrl'
},
restrict: 'E',
templateUrl: './../../js/directives/myVideoControl.html',
link: function (scope, element, attrs) {
scope.playVideo = function(){
var v = document.getElementsByTagName("video")[0];
v.play();
}
scope.pauseVideo = function(){
var v = document.getElementsByTagName("video")[0];
v.pause();
}
}
}
});
Will greatly appreaciate if anyone can point out if I am doing anything wrong here.
Thanks.
It looks like the problem you are having is that you are looking up the element by tag name. Basically, every element in your dom with the tag <video> is going to be effected by any use of your directive.
The idea with directives, is that they provide direct access to the element that the directive was assigned. In your case element inside the link function parameters. So you need to reference the individual associated elements like this:
var v = element[0];
v.play();
If you have assigned the directive on a parent element, and want all children, then use the find() jqLite function on the directive element:
var v = element.find('video')[0];
v.play();
var v = document.getElementsByTagName("video")[0];
You are selecting the first video tag in the entire page.
instead get the element inside your template,something like
element.find('video')[0]

Get the bounding rect for an element outside of a directive

Inside a directive I want to get the result of getBoundingClientRect() for a DOM element that is no where near the element of the directive.
How should I go about this? Service that just returns that object? Is it OK to have DOM logic in a service?
I'd suggest passing the id of the element the directive needs to interact with as an attribute to the directive. Then use the document object to get a handle to that element.
See: http://jsfiddle.net/afX63/6/.
You'll obviously need to work directly with the raw DOM element to access the information you're interested in.
Your markup:
<my-directive handle-id="thatOne"/>
<div id="thatOne">Your directive can find this element easily now</div>
You directive:
app.directive('myDirective', function () {
return {
link: function (scope, elem, attrs) {
var theHandle = angular.element(document.getElementById(attrs.handleId));
theHandle.text('Changed it');
}
}
});

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

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.

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();
});
});
}
});

Resources