AngularJS: Add attribute, compile directive - angularjs

Is it possible to somehow add directive as simple attribute in compile function and let angular handle compiling of added directive?
Provided example bellow obviously does not work, but my proper question would be, what is the cleanest way to achieve that?
var app = angular.module('app', []);
app.directive('testOne', function ($compile) {
return {
restrict: 'A',
priority: 10000,
compile: function (element, attrs) {
element.attr('test-two', '');
}
};
});
app.directive('testTwo', function () {
return {
restrict: 'A',
priority: 10,
compile: function (element, attrs) {
console.log(2);
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div test-one></div>
</div>

In the link phase, you can call $compile(element)(scope) to recompile the element to allow AngularJS to pick up the newly added directive. However, to prevent an infinite loop (since your initial directive will also be re-compiled), you should remove the initial directive's attribute first:
link: function (scope, element) {
element.removeAttr('test-one');
element.attr('test-two', '');
$compile(element)(scope);
}
Edit: You may also want to set terminal: true on your initial directive to prevent other directives from kicking in before your attribute massaging is done. You may also have to play around with the priority of the directive for similar reasons.

Related

Directive view does not get updated while updating scope variable in post link

I am not able to get my view updated while updating scope variable in post link function.
Following is the use of my directive.
<my-directive color='purple'>
</my-directive>
Following is the definition of my directive.
app.directive('myDirective', function () {
console.log('My Directive Called');
return {
restrict: 'E',
scope: {
localVar: '#color'
},
//template: '<span></span>', // When I enable this template it works fine.
/* This link way it is working fine.
link: function (scope, iElement, iAttrs) {
console.log(iElement);
iAttrs.color = 'red';
}*/
//This is not working Reason don't know.
compile: function (tElement, tAttrs) {
var spanElem = angular.element('<span> {{ localVar }} </span>');
spanElem.attr('color', tAttrs.color);
tElement.replaceWith(spanElem);
return function (scope, iElement, iAttrs) {
iAttrs.color = 'red';
};
}
};
});
I want to know the reason why this code is not working. It will work if I specify the template property in directive definition object. But I want to know what is going wrong in above code.
Please help me.
It's much easy if you do somehting like this:
JSFiddle
angular.module('myApp', [])
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
localVar: '#color'
},
template: '<span> {{ localVar }} </span>'
};
});
Without calling link function there is no two way data binding between template created by compile function and scope.
That's why when you turn on link function you get the desired result.
From angular docs.Please read this point.
HTML compilation happens in three phases:
$compile traverses the DOM and matches directives.
If the compiler finds that an element matches a directive, then the directive is added to the list of directives that match the DOM element. A single element may match multiple directives.
Once all directives matching a DOM element have been identified, the compiler sorts the directives by their priority.
Each directive's compile functions are executed. Each compile function has a chance to modify the DOM. Each compile function returns a link function. These functions are composed into a "combined" link function, which invokes each directive's returned link function.
$compile links the template with the scope by calling the combined linking function from the previous step. This in turn will call the linking function of the individual directives, registering listeners on the elements and setting up $watchs with the scope as each directive is configured to do.
The result of this is a live binding between the scope and the DOM. So at this point, a change in a model on the compiled scope will be reflected in the DOM.
EDIT CODE :
If you want to do it withour compile and link function,try to use isolated scope
EDIT CODE 2:
.directive('myDirective', function () {
console.log('My Directive Called');
return {
restrict: 'E',
scope: {
localVar: '#color'
},
template : '<span> {{ localVar }} </span>'
};
});
HTML :
<my-directive color='purple'>
</my-directive>
EDIT CODE 3:
directive('myDirective', function () {
console.log('My Directive Called');
return {
restrict: 'EA',
template: '<span>{{ localVar }}</span>', // When I enable this template it works fine.
compile: function (tElement, tAttrs) {
return {
post: function postLink(scope, iElement, iAttrs, controller) {
scope.localVar = 'red';
}
}
}
};
})

angularjs - is it not possible to add ng- attributes on a directive?

What I would like to be able to do is "wrap" the behavior of an ng-hide for a "permissions" directive... so I can do the following
Hide me
All is fine if I decide to simply "remove" the element from the dom; however, if I try to add an ng-hide and then recompile the element. Unfortunately, this causes an infinite loop
angular.module('my.permissions', []).
directive 'permit', ($compile) ->
priority: 1500
terminal: true
link: (scope, element, attrs) ->
element.attr 'ng-hide', 'true' # ultimately set based on the user's permissions
$compile(element)(scope)
OR
angular.module('my.permissions', []).directive('permit', function($compile) {
return {
priority: 1500,
terminal: true,
link: function(scope, element, attrs) {
element.attr('ng-hide', 'true'); // ultimately set based on the user's permissions
return $compile(element)(scope);
}
};
});
I've tried it without the priority or terminal to no avail. I've tried numerous other permutations (including removing the 'permit' attribute to prevent it from continually recompiling, but what it seems to come down to is this: there doesn't seem to be a way to modify an element's attributes and recompile inline through a directive.
I'm sure there's something I'm missing.
This solution assumes that you want to watch the changes of the permit attribute if it changes and hide the element as if it was using the ng-hide directive. One way to do this is to watch the permit attribute changes and then supply the appropriate logic if you need to hide or show the element. In order to hide and show the element, you can replicate how angular does it in the ng-hide directive in their source code.
directive('permit', ['$animate', function($animate) {
return {
restrict: 'A',
multiElement: true,
link: function(scope, element, attr) {
scope.$watch(attr.permit, function (value){
// do your logic here
var condition = true;
// this variable here should be manipulated in order to hide=true or show=false the element.
// You can use the value parameter as the value passed in the permit directive to determine
// if you want to hide the element or not.
$animate[condition ? 'addClass' : 'removeClass'](element, 'ng-hide');
// if you don't want to add any animation, you can simply remove the animation service
// and do this instead:
// element[condition? 'addClass': 'removeClass']('ng-hide');
});
}
};
}]);
angular.module('my.permissions', []).directive('permit', function($compile) {
return {
priority: 1500,
terminal: true,
link: function(scope, element, attrs) {
scope.$watch(function(){
var method = scope.$eval(attrs.permit) ? 'show' : 'hide';
element[method]();
});
}
};
});
I'm using this directive. This works like ng-if but it checks for permissions.
appModule.directive("ifPermission", ['$animate', function ($animate) {
return {
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function ($scope, $element, $attr, ctrl, $transclude) {
var block, childScope;
var requiredPermission = eval($attr.ifPermission);
// i'm using global object you can use factory or provider
if (window.currentUserPermissions.indexOf(requiredPermission) != -1) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
clone[clone.length++] = document.createComment(' end ifPermission: ' + $attr.ngIf + ' ');
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when it's template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
}
};
}]);
usage:
<div if-permission="requiredPermission">Authorized content</div>

Using a custom function as an expression

i wrote a function called getDepth(object) that gives me the depth of an object and then returns a string, for example if the depth of the object was 3 it will return the string "sub-sub-sub", and in my directive template i want to call that function from ng-class such as
'<span ng-class="getDepth(item)">{{item.content}}</span>'
but i am not sure where to put that function in my directive, should it just be inside the link function?
This is typically the job for a controller. So you can create an anonymous controller in your directive and place it there, or in the scope of the parent controller looking after this section of code - that is assuming of course one exists.
Maybe though you would like to reuse this functionality later in the app, so I recommend placing it high on the controller tree to allow others to inherit its function.
The linker functions job is strictly DOM manipulation, and this is not DOM manipulation this is a function returning a string and the ng-class directive in turn does the DOM manipulation.
If you check the docs:
Directives that want to modify the DOM typically use the link option. link takes a function with the following signature, function link(scope, element, attrs) { ... } where:
.directive('myCurrentTime', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}...
As you can see the link function is changing the DOM.
So to answer the question, this is how the directive should be structured.
app.directive('myDirective', function() {
var controller = function($scope) {
$scope.getDepth = function (item) {
return "text-success";
};
}
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '='},
controller: controller // or this can be the name of an outside reference controller as well - which i prefer for unit testing and reusability purposes.
}
};
}
]);
if you want to let this control to your directive instead controller you can put it in the link function, an example directive should be like this...
app.directive('myDirective', ['$compile',
function($compile) {
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '=item'},
link: function(scope, element, attrs) {
scope.getDepth = function (item) {
return "text-success";
};
}
};
}
]);
here is PLUNKER

dynamically added directive in AngularJS

I'm writing a custom directive. I want the directive to add an ng-click attribute to the element
attrs.$set('ng-click','clicked()');
I have tried adding the ng-click directive in compile function, and pre and post link functions. The attribute is added but doesn't work. I appreciate any insights. Thanks!
.directive('myDir', function () {
return{
compile: function (tElement, tAttrs, transclude) {
//tAttrs.$set('ng-click','clicked()');
return {
pre: function (scope, element, attrs) {
//attrs.$set('ng-click','clicked()');
},
post: function (scope, element, attrs) {
//attrs.$set('ng-click','clicked()');
scope.clicked = function(){
console.log('clicked!!!');
};
}
};
}
};
});
You've added the attribute via jQuery so Angular doesn't know about it. The quick answer is to wrap the call in a scope.$apply:
scope.$apply(function() {
tAttrs.$set('ng-click','clicked()');
});
So Angular knows you changed it.
But for other approaches that might work more cleanly with Angular look at What is the best way to conditionally apply attributes in Angular?

How do I pass an array into a directive without it turning into a string?

Righty, so I'm just getting into directives and they seem pretty awesome. I ran into a problem though:
I need to pass an array of images into a directive so I can filter them by certain criteria. Here's my html invoking the directive:
<img cover="{{challenge.images}}">
This is my directive:
myproject.directive('cover', function() {
return {
link: function ($scope, element, attrs) {
console.debug("attrs.cover", Array(attrs.cover));
}
};
});
The output is a String. is there a way to prevent attr turning into a String?
I'm assuming here that you don't want to create isolated scope, so:
myproject.directive('cover', function($parse) {
return {
link: function ($scope, element, attrs) {
var covers = $parse(attrs.cover)($scope);
console.debug("attrs.cover", covers);
}
};
});
and then use the directive like so:
<img cover="challenge.images">

Resources