Use $compile inside Angular link function AND access directive arguements - javascript

I am making a directive in angular.js 1.x
I call the directive as follows:
<mydirective dirarg={{value-1}}></mydirective>
I would like to create the directive by putting code to manipulate the DOM in the directive's link function. The structure of the DOM generated by the link function is dependant on the value of dirarg, and I would like some elements to have a ng-click attribute.
I have managed to get ng-clicks to work by doing the following:
app.directive('calendar',function($compile){
return{
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>hi</button>")(scope));
}
}
});
When I click the button generated by this directive, the function testt() runs. However, the call to testt() breaks if I try to access dirarg.
app.directive('calendar',function($compile){
return{
scope:{
dirarg: '#'
},
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>"+scope.dirarg+"</button>")(scope));
}
}
});
This code now populates the text of the button with dirarg, but the ng-click functionality breaks. Does somebody know how I can both have ng-click working, and access the arguments to the directive?
To be clear, this button is just an example. My actual situation is a lot more complicated than a button, so don't tell me better ways to make buttons in angular.

When add scope property to directive option it makes isolated scope for directive, you need pass testt function inside directive too, or define testt function inside directive link functiontion
<mydirective dirarg={{value-1}} testt="testt"></mydirective>
app.directive('calendar',function($compile){
return{
scope:{
dirarg: '#',
testt: '='
},
link: function(scope, element, attributes){
element.append($compile("<button ng-click='testt()'>"+scope.dirarg+"</button>")(scope));
}
}
});

Related

attribute directive not working for ajax loaded element?

I'm dealing with legacy code. My page is composed of three partial views, one for header, one for footer, one for the content. I have this element with my-directive in my footer:
<a my-directive>
<img>
</a>
My footer is rendered at the same time with the others on page load. However, in my header, I have #products_menu whose content is loaded via ajax:
// calls a route to do some processing before returning the view
// to be rendered inside #products_menu
$('#products_menu').load(...);
#products_menu contains the same element with the same directive:
<a my-directive>
<img>
</a>
This is my directive:
angular
.module('module1')
.directive('myDirective', ['$rootScope', 'ModalService', '$compile',
function($rootScope, ModalService, $compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
console.log(element);
element.on('click', function(e) {...}
} ...
The problem is when I click on the element in the footer, it fires. But when I click on the element in the header, it doesn't. I'm assuming it's because the element was loaded dynamically, and the attribute was not bound during the time angular was compiling.
AM I right in my assumption and if so, is there a workaround?
So just dumping the html via using jquery or DOM manipulation would not intimate angular to do its work. You have to tell angular about this or better compile the DOM using $compile service. Angular things will automatically triggers in for that template
$('#products_menu').load(..., function() {
$compile($('#products_menu'))($scope);
})
I'd recommend to mix jQuery with angular, best way to handle this in angular way is using ng-include directive
<div id="products_menu" ng-include="'template.html'">
</div>

Access the html content of directive in parent controller

I have an overlay and I set its position manually by accessing the html content and getting the height, width etc.
Now, I have converted the overlay to a directive. But I cant access the html content of the directive itself to update the position.
What have I tried: I tried using replace:true in the directive. But when I try to access the div - I still get the directive element rather than the directive html content.
Eg:
My Directive:
<my-dir somevalue='something'>
</my-dir>
Directive HTML:
<div class="overlay">
<span>{{somevalue}}</span>
</div>
Now when I try to access the directive element, I want to access the class 'overlay'
First, in your directive template, you should probably be referring to somevalue, not something, since presumably somevalue is what's being set on the directive's scope.
Then to access the .overlay child element, in your directive's link function you can use the querySelector method on the directive's native DOM element, like so:
link: function (scope, el, attrs) {
var overlayElement = el[0].querySelector('.overlay');
}
The el that's passed into the link function is the jqLite-wrapped directive element, so el[0] gets you the native DOM element.
Here's a fiddle demonstrating this approach.

Angular attribute directive - add an ng-repeat below current directive

If I had an attribute directive, for example something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
where let's say that ngModel and customModel are arrays. Is there a way I can, within the directive's code, add a piece of html below the directives element which could have access to the scope of the directive and be able to reference the customModel so that in the end it looks something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
<div><!-- this code gets added by the custom-directive directive and uses it's scope -->
<span ng-repeat="item in customDirectiveCtrl.customModel" ng-bind="item.property"></span>
</div>
I know I can add html manually using jqLite, however this html doesn't have access to directive scope. The reason I don't want to convert the custom-directive directive from attribute directive to element directive is because it makes it way more difficult to add attributes such as id, name, required, disabled,... to underlying template elements (in the case of this example, a select element)
EDIT: as requested here's an example of how to add an element after the directives element:
{
restrict: 'A',
require: 'ngModel',
scope: { customModel: '=customDirective' },
link: function(scope, element, attrs, ngModel) {
//element.after('<div></div>'); //this adds a div after the directives element
element.after('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>'); //this will add the html in the string, but will not interpret the angular directives within since (i assume) that it is not bound to any scope.
}
}
Any angular component/directive added like this will not work properly or at all.
If you are injecting new HTML into the page in your directive, and you need that HTML to use angular directives (ng-repeat, ng-bind, etc) then you will need to use the $compile service to make angular aware of your new DOM elements. In your case, you would inject the $compile service into your directive and then use it like this:
link: function(scope, element, attrs, ngModel) {
//create the new html
var newElement = angular.element('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>');
//compile it with the scope so angular will execute the directives used
$compile(newElement)(scope); //<-this is the scope in your link function so the "customModel" will be accessible.
//insert the HTML wherever you want it
element.after(newElement);
}

Angular Using Jquery Plugin issues...dynamic dropdown data

I am using this jquery plugin selectric and trying to hook it up with my angular drop down.
It works fine if my html for the dropdown has the options hardcoded in the html and I just do
$('select, .select').selectric();
But if I load my dropdown data dynamically like this
<select class="form-control" ng-model="myAddress" ng-options="address.addressLines for address in search.result.addresses" size="{{numAddressOptions}}">
</select>
I get a javascript error in the selectric plugin
TypeError: $li[index] is undefined
I tried adding the selectric plug in as a directive
<select class="form-control" ng-model="myAddress" ng-options="address.addressLines for address in search.result.addresses" size="{{numAddressOptions}}" selectric>
</select>
and my directive
.directive('selectric', function(){
'use strict';
return{
restrict: 'AE',
link: function(scope, element, attrs) {
$(element).selectric();
}
};
});
Any Ideas of what could be going wrong here ?
Since ng-options is a priority 0 directive your directive's link function cannot be guaranteed to fire after it: Angular 1.3.7 Compile Docs.
priority
When there are multiple directives defined on a single DOM element,
sometimes it is necessary to specify the order in which the directives
are applied. The priority is used to sort the directives before their
compile functions get called. Priority is defined as a number.
Directives with greater numerical priority are compiled first.
Pre-link functions are also run in priority order, but post-link
functions are run in reverse order. The order of directives with the
same priority is undefined. The default priority is 0.
To achieve the desired behavior you will want to pass your objects into a directive that creates the select element in its template. This is a very good example: https://stackoverflow.com/a/14586825/1861459. This should move your link method invocation to the correct part of the directive lifecycle (after the compile is completed for the directives in your template).
You would also change restrict: 'AE', to restrict: 'E', and change your HTML to something like <selectric addresses="{{ search.result.addresses }}" ...></selectric>

Transclude in AngularJS without adding new element

Is there any way to transclude some content into a directive without adding extra elements.
For example
directive:
{
scope: {
someParam: "="
},
link: function(scope, element, attrs){
//do something
},
transclude: true,
template:'<div ng-transclude></div>'
}
source html:
<div my-directive some-param="somethingFromController">
my transcluded content: {{somethingElseFromController}}
</div>
With this example an extra div gets added to the markup. Normally this would be fine but I'm trying to use this directive inside a table so adding a div tag screws things up.
I also tried not specifying transclude or template which gets rid of the extra div tag but now {{somethingElseFromController}} cannot be found as the "transcluded" content is in an isolated scope. I know I could just get the parameters for my directive from the attrs object in the linking function instead of creating an isolated scope but I'd rather avoid needing to evaluate strings with scope.$apply().
Anyone know how to accomplish this?
Thanks!
What #Vakey answered is what I was searching for.
But as today, the Angular documentation says:
The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
So I used instead the controller (for the moment) and its $transclude function, as part of the example shown on the $compile documentation:
controller: function($scope, $element, $transclude) {
var transcludedContent, transclusionScope;
$transclude(function(clone, scope) {
$element.append(clone);
transcludedContent = clone;
transclusionScope = scope;
});
},
This actually is possible with Angular. Directives such as ng-repeat do this. Here is how you do it:
{
restrict: 'A',
transclude: true,
compile: function (tElement, attrs, transclude) {
return function ($scope) {
transclude($scope, function (clone) {
tElement.append(clone);
});
};
}
};
So what's going here? During linking, we are just appending the clone, which is the element we are trying to transclude, into the directive's element. Angular will apply $scope onto the clone element so you can do all the angular goodness inside that element.
To elaborate on #rob's post...
Transclusion requires that Angular creates an element that is a clone of the content of whatever tag the directive is/lives on... If the content is text, it will wrap it in a span.
This is so it has a DOM element to apply the scope to when $compile is called.
So, basically transclude adds an element for the same reason you can't $compile('plain text here {{wee}}').
Now, you can do something sort of like what you're trying to do with $interpolate, which allows you to apply a scope to bindings in a string like "blah {{foo}}".... but since I'm really not sure what you're trying to do, I can't really give you a specific example.

Categories

Resources