AngularJS - adding directive dynamically to an element - angularjs

I have created a directive that check if data was entered to an HTML element in the following way:
var myApp = angular.module('myApp', []);
myApp.directive("uiRequired", function () {
return function (scope, elem, attrs) {
elem.bind("blur", function () {
var $errorElm = $('#error_testReq');
$errorElm.empty();
if (angular.isDefined(attrs) && angular.isDefined(attrs.uiRequired) && attrs.uiRequired == "true" && elem.val() == "") {
$errorElm.append("<li>Field value is required.</li>");
$errorElm.toggleClass('nfx-hide', false);
$errorElm.toggleClass('nfx-block', true);
}
else
{
$errorElm.toggleClass('nfx-hide', true);
$errorElm.toggleClass('nfx-block', false);
}
});
};
});
A working example can be seen here
My question:
Is there a way of adding the directive (uiRequired) I have created dynamically to elements on screen on document ready.
I want to put the new directive on selected HTML elements according to pre-defined list I have. I can not know in advance on which field this directive has to be on.
So I have to put it while page is rendering.
I have tried putting it dynamically myself while page is loading, however AngularJS did interpret it.
I could not find an example on the internet that does that.
Can anyone help me?

You can dynamically add directives to a page during the compilation phase when Angular is walking the DOM and matching elements to directives. Each step of the compilation process may transform the DOM tree ahead of it, but you should never modify elements that Angular has already compiled. This point is important to remember because adding directives to elements that have already been walked will have no effect. Of course, there ways around this. For example, you could re-compile and re-link previously walked elements. However I strongly advise against this as it may lead to unintended side effects such as memory leaks, and slow performance.
To dynamically add uiRequired directives, you can create a parent directive - let's call it uiRequiredApplier.
app.directive('uiRequiredApplier', function($scope) {
return {
restrict: 'A',
compile: function(element, attr) {
// you can apply complex logic figure out which elements
// you want to add the uiRequired attribute to
$('input', element).attr('uiRequired','');
return function(scope, element, attr) {
}
}
}
});
You can then apply the attribute like this:
<div ui-required-applier>
<input type="text" ng-model="name" />
</div>
When the uiRequiredApplier is compiled, it will dynamically add uiRequired attributes to selected elements using jQuery that have not been compiled yet. And when Angular walks the DOM, eventually it will compile and link the uiRequired attributes, which will add the desired validation behavior.

Related

How to modify transcluded content before compile inside directive?

What I want to do, is to handle transclude by hand and modify the content before I insert into the DOM:
return {
restrict: 'E',
transclude: true,
template: '<HTML>',
replace: true,
link: function(scope, element, attrs, ngModelCtrl, $transclude) {
var caption = element.find('.caption');
$transclude(function(clone) {
console.log(clone);
clone.filter('li').addClass('ng-hide'); // this don't work
clone.addClass('ng-hide'); // same this one
clone.attr('ng-hide', 'true'); // same this one
$compile(clone)(scope.$new()).appendTo(caption);
caption.find('li').addClass('ng-hide'); // and this
});
}
}
In angular.js source I found this example:
var templateElement = angular.element('<p>{{total}}</p>'),
scope = ....;
var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) {
//attach the clone to DOM document at the right place
});
//now we have reference to the cloned DOM via `clonedElement`
but when I add clonedElement.appendTo(caption); inside link function it only add comment with ng-repeat inside.
I need this because I need to hide all elements in this case
<dropdown>
<li ng-repeat="item in items"><a>{{item.label}}</a></li>
</dropdown>
I need to modify the template before compile or DOM after ng-repeat is expanded. Before would be better because I will be able to add logic using ng-hide directive instead of ng-hide class.
I realise it's been a long time since this question was posted, but I hope you may find the following useful.
I've been quite long and heavily in this (transclusion) business, I tried a few ways to achieve what you #jcubic need and finally I came across a solution which is really robust and quite simple.
...
replace: false,
transclude: false,
compile: function( tElement, tAttributes ) {
// store your "transcluded" content of the directive in the variable
var htmlContent = tElement.html();
// then remove it
tElement.html('');
return function postLink(scope, elem, attrs) {
// then html var is available in your link!
var $html = $('<div />',{ html:htmlContent }); // for much easier manipulation (so you can use DOM functions - you can also manipulate directly on htmlContent string)
// so you can manipulate the content however you want
scope.myVariable = true;
$html.find('li').attr('ng-hide', 'myVariable'); // add native directive
$html.removeClass('inner-content').addClass('my-inner-content'); // add/remove class
$html.find('#myElement').attr('my-directive',''); // add custom directive etc. etc.
// after you finished you just need to compile your html and append your directive element - also however you want
// you also convert back $html to the string
elem.append( $compile( $html.html() )(scope) ); // append at the end of element
/* or:
elem.find('.my-insert-point').html( $compile( $html.html() )(scope) ); // append the directive in the specific point
elem.find('[my-transclude]').html( $compile( $html.html() )($parent.scope) ); // once the scope:true it will be the same as native transclusion ;-)
scope.variable = $html.html(); // or you can probably assign to variable and use in your template with bind-html-compile (https://github.com/incuna/angular-bind-html-compile) - may need $sce.trustAsHtml
*/
}
}
...
So as you can see you have full control on your "transcluded" content and you don't even need transclusion! :-)
ps. I tested it with Angular 1.4. Not sure if it works with replace:true (I wasn's bother to test it as it's minor nuisance if it doesn't). You can use pre and post link as normally you'd use within compile function and you need to inject $compile service into your directive.
jcubic. You do not have to use $compile for what you are trying to do.
You can filter the transcluded element 'clone' and add css classes to the filtered nodes , but after that you have to append the modified clone to the template (it is identified by the 'element' attribute of the link function).
element.append(clone)
I created this jsfiddle for you.
If you still have other questions , please create a jsfiddle of your case.It Will be better to make an answer Thx
If you're using angular > 1.3 and ngTransclude in template, so you need to update not the clone, but transcluded DOM, eg:
elm.find('ng-transclude')
http://jsfiddle.net/jhqkxgos/
but be sure to compile found elements if you update some you need to access from controller

angularjs conditional directive only on desktop

I want to put a directive on an element only if it's not viewed from mobile. As it's an externally maintained plugin I don't want to modify the directive itself. What's the easiest way of doing this?
Create a directive that performs the detection (or receives it), adds the directive if not viewed from mobile, removes itself from the element and then compiles the element.
HTML:
<div not-on-mobile="external-directive">Hello.</div>
JS:
app.directive('notOnMobile', function($compile) {
// Perform detection.
// This code will only run once for the entire application (if directive is present at least once).
// Can be moved into the compile function if detection result needs to be passed as attribute.
var onMobile = false;
return {
compile: function compile(tElement, tAttrs) {
if (!onMobile) tElement.attr(tAttrs.notOnMobile, '');
tElement.removeAttr('not-on-mobile');
return function postLink(scope, element) {
$compile(element)(scope);
};
}
};
});
Demo: http://plnkr.co/edit/nJntmfiLZ20JCdSWiRDQ?p=preview

AngularJS Access DOM inside $watch function

I'm making a directive that resizes a div based on changes in the controller. I need to calculate the amount of available space left in the browser window when changes happen to the model. How do you pass in the element from the link function into the $watch function?
In short, how do I manipulate the DOM based on changes to the model?
var module = angular.module('cmsApp')
module.directive("changeWidth", function($timeout) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
width = element.width();
$scope.$watch('currentFolder', function(value){
// manipulate dom here
});
}
}
});
<!-- need to calculate the size of this -->
<div change-width class="col-md-9 right-pannel"></div>
I don't think Angular is even executing your directive based on your template code. It should be
<div change-width class="col-md-9 right-pannel"></div>
I know this is a source of errors if you are new to Angular. From the docs:
Angular uses name-with-dashes for its custom attributes and camelCase
for the corresponding directives which implement them)

AngularJS - Find Element with attribute

I'm new to AngularJS. I've learned that I can find elements in the DOM using queries like the following:
var e = angular.element(document.querySelector('#id'));
var e = angular.element(elem.querySelector('.classname'));
This is useful for finding elements by ID, or by CSS class name. However, I need to be able to find an element using a different approach. I have an element that looks like the following:
<div my-directive class='myContainer'>...</div>
I can't query on 'myContainer' because of how much its reused. For that reason, I would like to find any element with the attribute 'my-directive'. How do I search the DOM and find any element that makes use of 'my-directive'?
Rather than querying the DOM for elements (which isn't very angular see "Thinking in AngularJS" if I have a jQuery background?) you should perform your DOM manipulation within your directive. The element is available to you in your link function.
So in your myDirective
return {
link: function (scope, element, attr) {
element.html('Hello world');
}
}
If you must perform the query outside of the directive then it would be possible to use querySelectorAll in modern browers
angular.element(document.querySelectorAll("[my-directive]"));
however you would need to use jquery to support IE8 and backwards
angular.element($("[my-directive]"));
or write your own method as demonstrated here Get elements by attribute when querySelectorAll is not available without using libraries?
You haven't stated where you're looking for the element. If it's within the scope of a controller, it is possible, despite the chorus you'll hear about it not being the 'Angular Way'. The chorus is right, but sometimes, in the real world, it's unavoidable. (If you disagree, get in touch—I have a challenge for you.)
If you pass $element into a controller, like you would $scope, you can use its find() function. Note that, in the jQueryLite included in Angular, find() will only locate tags by name, not attribute. However, if you include the full-blown jQuery in your project, all the functionality of find() can be used, including finding by attribute.
So, for this HTML:
<div ng-controller='MyCtrl'>
<div>
<div name='foo' class='myElementClass'>this one</div>
</div>
</div>
This AngularJS code should work:
angular.module('MyClient').controller('MyCtrl', [
'$scope',
'$element',
'$log',
function ($scope, $element, $log) {
// Find the element by its class attribute, within your controller's scope
var myElements = $element.find('.myElementClass');
// myElements is now an array of jQuery DOM elements
if (myElements.length == 0) {
// Not found. Are you sure you've included the full jQuery?
} else {
// There should only be one, and it will be element 0
$log.debug(myElements[0].name); // "foo"
}
}
]);
Your use-case isn't clear. However, if you are certain that you need this to be based on the DOM, and not model-data, then this is a way for one directive to have a reference to all elements with another directive specified on them.
The way is that the child directive can require the parent directive. The parent directive can expose a method that allows direct directive to register their element with the parent directive. Through this, the parent directive can access the child element(s). So if you have a template like:
<div parent-directive>
<div child-directive></div>
<div child-directive></div>
</div>
Then the directives can be coded like:
app.directive('parentDirective', function($window) {
return {
controller: function($scope) {
var registeredElements = [];
this.registerElement = function(childElement) {
registeredElements.push(childElement);
}
}
};
});
app.directive('childDirective', function() {
return {
require: '^parentDirective',
template: '<span>Child directive</span>',
link: function link(scope, iElement, iAttrs, parentController) {
parentController.registerElement(iElement);
}
};
});
You can see this in action at http://plnkr.co/edit/7zUgNp2MV3wMyAUYxlkz?p=preview

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