AngularJS directive only when the condition is true - angularjs

I am going to have a contextmenu directive in ng-repeat items.
Based on whether a condition is true, the directive should be applied.
How do I put a condition like only when item.hasMenu == true then apply the directive ?
<ul ng-controller="ListViewCtrl" >
<li contextmenu ng-repeat="item in items">{{item.name}} </li>
</ul>
EDIT
This seems to have worked for me. First the directive.
app.directive('menu',function(){
return {
restrict : 'A',
link : function(scope,element,attrs){
if(scope.hasMenu){
element.contextmenu({
menu:[
{title:"Remove" , "cmd" : "remove"},
{title:"Add" , "cmd" : "add"},
],
select:function(event,ui){
//alert("select " + ui.cmd + " on" + ui.target.text());
if (ui.cmd ==='remove'){
alert('Remove selected on ' + scope.item);
}
if (ui.cmd ==='add'){
alert("Add selected");
}
}
});
}
}
}
}
);
Then the html
<ul ng-controller="ListViewCtrl" >
<li menu ng-repeat="item in items">{{item.name}} </li>
</ul>

Can you do something like this, using ng-if?
<ul ng-controller="ListViewCtrl" >
<li ng-repeat="item in items">
<span>{{item.name}}</span>
<div contextmenu ng-if="item.hasMenu"></div>
</li>
</ul>
Here are the docs for ng-if.
EDIT:
If you are driving the context menu off of a class, you should be able to do this:
<ul ng-controller="ListViewCtrl" >
<li ng-class="{'hasmenu': item.hasMenu}" ng-repeat="item in items">{{item.name}} </li>
</ul>

I think this is pretty tricky if you don't want to change your DOM structure. If you could just place your contextmenu directive on a sub DOM node inside the <li> things would be a lot easier.
However, let's assume you can't do that and let's also assume that you don't own the contextmenu directive so that you can't change it to your needs.
Here is a possible solution to your problem that might be a bit hackish (actually I don't know!)
'use strict';
angular.module('myApp', [])
.controller('TestController', ['$scope', function($scope) {
$scope.items = [
{name:1, hasMenu: true},
{name:2, hasMenu: false },
{name:3, hasMenu: true}
];
}])
.directive('contextmenu', function(){
return {
restrict: 'A',
link: function(scope, element){
element.css('color', 'red');
}
}
})
.directive('applyMenu', ['$compile', function($compile){
return {
restrict: 'A',
link: function(scope, element){
if (scope.item.hasMenu){
//add the contextmenu directive to the element
element.attr('contextmenu', '');
//we need to remove this attr
//otherwise we would get into an infinite loop
element.removeAttr('apply-menu');
//we also need to remove the ng-repeat to not let the ng-repeat
//directive come between us.
//However as we don't know the side effects of
//completely removing it, we add it back after
//the compile process is done.
var ngRepeat = element.attr('ng-repeat');
element.removeAttr('ng-repeat');
var enhanced = $compile(element[0])(scope);
element.html(enhanced);
element.attr('ng-repeat', ngRepeat);
}
}
}
}]);
I faked the contextmenu directive to just change the color to red just so that we can see it's taking place.
Then I created an apply-menu attribute directive. This directive than checks if the hasMenu property is true and if so hooks in and adds the contextmenu directive and does a manual $compile process.
However, what worries me a bit about this solution is that I had to temporally remove the ng-repeat directive (and also the apply-menu directive) to get the $compile process to act the way we want it to act. We then add the ng-repeat directive back once the $compile has been made. That is because we don't know the side effects of removing it entirely from the resulting html. This might be perfectly valid to do, but it feels a bit arkward to me.
Here is the plunker: http://plnkr.co/edit/KrygjX

You can do this way
angularApp.directive('element', function($compile) {
return {
restrict: 'E',
replace: true,
transclude: true,
require: '?ngModel',
scope: 'isolate',
link: function($scope, elem, attr, ctrl) {
$scope.isTrue = function() {
return attr.hasMenu;
};
if($scope.isTrue())
//some html for control
elem.html('').show();
else
//some html for control
elem.html('').show();
$compile(elem.contents())($scope);
}
};
});

Related

How to pass a variable through scope into directive

Inside an element directive named tabset, i am using element directive named tab twice which requires tabset.
I am doing a console.log(attr.newvar) from link of tabset.
newvar is the value passed into the scope of the tabset directive.
So the tabset gets called 2 times (which i suppose is correct), and hence output is consoled twice.
1st time, the console output is giving the correct output, but the 2nd time it is showing newvar as undefined .
but i am not able to access newvar through scope.newvar.In case of console.log(scope.newvar), i get output as undefined twice.
Why is this happening ?
HTML snippet
<tabset newvar="black">
<tab></tab>
<tab></tab>
</tabset>
JS snippet
.directive('tab',function(){
return{
restrict:'E',
require:'^tabset',
transclude:true,
scope:{
heading:"#"
},
template:'<div ng-show="active" ng-transclude></div>',
link:function(scope,elem,attr,tabsetCtrl){
scope.active = false;
tabsetCtrl.add(scope);
}
}
})
.directive('tabset',function(){
return{
restrict:'E',
scope:{
item:"=",
newvar:"#"
},
transclude:true,
templateUrl:'/partials/tabset/tabset.html',
bindToController:true,
controllerAs:'tabset',
controller:function($scope){
var self = this;
self.tabs = []
self.add = function add(tab){
self.tabs.push(tab);
if(self.tabs.length === 1){
tab.active = true;
}
}
self.click = function click(selectedTab){
angular.forEach(self.tabs,function(tab){
if(tab.active && tab !== selectedTab)
tab.active = false;
})
selectedTab.active = true;
}
},
link:function(scope,elem,attr,optionsCtrl){
console.log(scope.newvar)
scope.resetInput = function(){
console.log("in resetInput")
optionsCtrl.firstBox = "e"
scope.item = "";
}
}
}
})
tabset template
<ul class="nav nav-tabs" ng-class="'{{newvar}}'" >
<li class='' ng-repeat="tab in tabset.tabs" >
<a href="" ng-click="tabset.click(tab);resetInput();" ng-class='{"tab-active":tab.active,"tab-inactive":tab.active === false}'> {{tab.heading}}</a>
</li>
</ul>
<ng-transclude>
</ng-transclude>
As you are using bindToController: true you could access you controller scope from link function 4th parameter which is controller, which will give you access to the directive controller this which is using controllerAs syntax in it.
link: function(scope, elem, attr, ctrl) {
console.log(ctrl.newvar);
//ctrl.newvar = attr.newvar;
}
Update
As you want to add class to your tab ul element. I think you shouldn't use ng-class there. ng-class used when conditionally show/hide any class in html. You should use plane {{}} interpolation in you class attribute. While accessing scope variable you need to use tabset. because you are using controllerAs syntax with its alias. I tried to add class with ng-attr-class but the class gets added but other two classes was getting removed nav nav tabs that's the reason behind using {{tabset.newvar}}.
Template
<ul class="nav nav-tabs {{tabset.newvar}}">
<li class='' ng-repeat="tab in tabset.tabs" >
<a href="" ng-click="tabset.click(tab);resetInput();" ng-class='{"tab-active":tab.active,"tab-inactive":tab.active === false}'> {{tab.heading}}</a>
</li>
</ul>
Working Plunkr
You can use scope.tabset to reference the controller and thereby give you access to your variable. Like this:
link: function(scope, elem, attr) {
console.log(scope.tabset.newvar);
}

Detect if a transclude content has been given for a angularjs directive

I have a directive (a progressbar) which should have two possible states, one without any description and one with a label on the left side.
It would be cool to simple use the transcluded content for this label.
Does anyone know how I can add a class to my directive depending whether a transclude content has been given or not?
So I want to add:
<div class="progress" ng-class="{withLabel: *CODE GOES HERE*}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
Thanks a lot!
After release of Angular v1.5 with multi-slot transclusion it's even simpler. For example you have used component instead of directive and don't have access to link or compile functions. Yet you have access to $transclude service. So you can check presence of content with 'official' method:
app.component('myTransclude', {
transclude: {
'slot': '?transcludeSlot'
},
controller: function ($transclude) {
this.transcludePresent = function() {
return $transclude.isSlotFilled('slot');
};
}
})
with template like this:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude="slot"></span>
<div class="other">...</div>
</div>
Based on #Ilan's solution, you can use this simple $transclude function to know if there is transcluded content or not.
$transclude(function(clone){
if(clone.length){
scope.hasTranscluded = true;
}
});
Plnkr demonstrating this approach with ng-if to set default content if nothing to transclude: http://plnkr.co/hHr0aoSktqZYKoiFMzE6
Here is a plunker: http://plnkr.co/edit/ednJwiceWD5vS0orewKW?p=preview
You can find the transcluded element inside the linking function and check it's contents:
Directive:
app.directive('progressbar', function(){
return {
scope: {},
transclude: true,
templateUrl: "progressbar.html",
link: function(scope,elm){
var transcluded = elm.find('span').contents();
scope.withLabel = transcluded.length > 0; // true or false
}
}
})
Template:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
You can also create your custom transclusion directive like so:
app.directive('myTransclude', function(){
return {
link: function(scope, elm, attrs, ctrl, $transclude){
$transclude(function(clone){
// Do something with this:
// if(clone.length > 0) ...
elm.empty();
elm.append(clone);
})
}
}
})
Based on the solution from #plong0 & #Ilan, this seems to work a bit better since it works with whitespace as well.
$transcludeFn(function(clonedElement) {
scope.hasTranscludedContent = clonedElement.html().trim() === "";
});
where previously <my-directive> </my-directive> would return that it has a .length of 1 since it contains a text node. since the function passed into $transcludeFn returns a jQuery object of the contents of the transcluded content, we can just get the inner text, remove whitespace on the ends, and check to see if it's blank or not.
Note that this only checks for text, so including html elements without text will also be flagged as empty. Like this: <my-directive> <span> </span> </my-directive> - This worked for my needs though.

AngularJS: How create a new directive joining existents directives?

I would like improve my current solution.
I have a big menu using <ul> and <li> tags. I need show to the user only the <li> tags that they have permission to access.
I have resolved this problem using two directives: ng-init and ng-show
...
<li ng-init="ok=hasPemission('item1')" ng-show="ok">
Item 1
</li>
...
I needed to use 'ng-init' to get a stable hasPermission result. I can't use ng-show="hasPemission('item1')" because 'hasPemission' returns a new 'promise' object and the ng-show has problem with not stable expressions ([$rootScope:infdig] infinit $digest loop).
Now, I wanted create a new directive that join this both directives. I created this one but I think that there is a way to reuse these two existents directives.
This is my new directive:
.directive('myPermissionShow',['$q','$animate','AccessControl',
function ($q, $animate, AccessControl) {
return {
restrict:'A',
scope: {
resourceName: '#myPermissionShow'
},
link: function ($scope, $element) {
var user = AccessControl.getLoggedUser();
AccessControl.hasPermission(user,$scope.resourceName).then(
function (value) {
// I copied the line below from ngShow directive:
$animate[value ? 'removeClass' : 'addClass']($element, 'ng-hide');
}
);
}
}
}])
So, my html changed to:
...
<li my-permission-show="item1">
Item 1
</li>
<li my-permission-show="item2">
Item 2
</li>
...
Is there a way to create this new directive reusing the directives 'ng-init' and 'ng-show'?
Something like this:
!!!NOT WORKING CODE!!!
.directive('myPermissionShow',['AccessControl',
function (AccessControl) {
return {
restrict:'A',
replaceAttribute: true, /* this property does not exists... */
template: 'ng-init="val=hasPermission(user,resourceName)" ng-show="val"',
scope: {
resourceName: '#myPermissionShow'
},
controller: function ($scope, $element) {
$scope.hasPemission = AccessControl.hasPermission;
$scope.user = AccessControl.getLoggedUser();
}
}
}])
!!!NOT WORKING CODE!!!

Adding a CSS class to element on ng-click

I have several arbitrary number of menu items on my page. Simplified they look like this.
...
I would like to add a particular class to an item that is being clicked so that its state changes compared to others.
I know I should do it sugin this kind of a pattern:
<a href=""
class="menu-item"
ng-class="{ active: clicked }"
ng-click="clicked = true">
...</a>
but the problem is that I can't use a single variable as I have an arbitrary number of items. I should either use X number of variables or use an array. But in any case how would I know which item goes with each variable/array index unless I manually enter those indices by hand?
What I'm missing
I'm missing element reference that I could use in ng-click so I could add a particular class on element itself. But that would somewhat bind $scope and UI even though I wouldn't be using any $scope function that would manipulate UI. I'd do it all in the view...
How am I supposed to do this?
A directive that solves this problem:
app.directive("markable", function() {
return {
scope: {}, // create isolated scope, so as not to touch the parent
template: "<a href='#' ng-click='mark()' ng-class='{ active: marked }'><span ng-transclude></span></a>",
replace: true,
transclude: true,
link: function(scope, elem, attrs) {
scope.marked = false;
scope.mark = function() {
scope.marked = true;
};
}
};
});
Use it as:
<a markable>Mark me</a>
Relevant fiddle: http://jsfiddle.net/9HP99/
The advantage of a directive is that it is very easy to use.
The same with a DOM-manupulating directive, through Angular's jQuery-ish interface:
app.directive("markable", function() {
return {
link: function(scope, elem, attrs) {
elem.on("click", function() {
elem.addClass("active");
});
}
};
});
Usage:
<a href="#" markable>Mark me</a>
Fiddle: http://jsfiddle.net/atE4A/1/
You could do something like this:
...
then you can use this.ClassName to get nth item.
But if you # of items is arbitrary, you may consider trying to format the list in a way where you could use ng-repeat, then it would be as easy as this:
...

AngularJS : Toggle to modify attribute in directive

In the project I am working on, I am applying a ui-sort via Angular on a to-do list and am trying to get a toggle to work for when a user is editing tasks. My current method of testing this toggle is employing the use of a button to toggle sorting on and off.
My strategy is this:
Employ an angular directive to generate an initial template with sorting on.
Add a button which, when clicked, modifies a scope variable in the controller ($scope.sortingEnabled) to toggle between true and false.
Inside my directive, I have a watch set on 'sortingEnabled' in a link function to add/remove the sorting attribute from a .
Here is the in todo.html before I tried employing a directive:
sortableOptions is a function written to re-order the todos on internal records.
<ul class="unstyled" ng-model="todos" ui-sortable="sortableOptions">
<!-- list items here via ng-repeat -->
</ul>
The following is the code in todo.html after my directive:
<sortable></sortable>
And my current draft for the directive inside todo-directives.js:
app.directive('sortable', function() {
var innerHtml = '<li ng-repeat="todo in todos" class="item">' +
'<span ng-model="todo.name" >{{todo.name}}</span> ' +
'</li>';
var link = function (scope, element, attrs) {
scope.$watch('sortingEnabled', function() {
if(scope.sortingEnabled === true) {
element.contents().attr("ui-sortable", "sortableOptions");
//needed else ui-sortable added as a class for <ul> initially for
//some reason
element.contents().removeClass("ui-sortable");
}
else {
element.contents().removeAttr("ui-sortable");
//needed else ui-sortable added as a class for <ul> initially for
//some reason
element.contents().removeClass("ui-sortable");
}
});
};
return {
restrict: 'E',
transclude: true,
template: '<ul class="unstyled" ng-model="todos" ui-sortable="sortableOptions" ng-transclude>' + innerHtml + '</ul>',
link: link
};
});
This code works in the source code view of Chrome's debugger, but the view does not properly refresh. I have tried scope.$apply() within the watch function but get a $digest already running error. I have also tried $compile, but my understanding of how that works is severely lacking, so I get errors of which I do not remember.
Am I missing something crucial, or doing things incorrectly? I am unsure, as my understanding is low, being that I have been leaning Angular for a few weeks. Any help would be greatly appreciated!
The angular directive supports watching when the sortable options change:
scope.$watch(attrs.uiSortable, function(newVal, oldVal){
So all you had to do was look at the jqueryui sortable documentation, and update the correct property on the plugin.
Plunker: http://plnkr.co/edit/D6VavCW1BmWSSXhK5qk7?p=preview
Html
<ul ui-sortable="sortableOptions" ng-model="items">
<li ng-repeat="item in items">{{ item }}</li>
</ul>
<button ng-click="sortableOptions.disabled = !sortableOptions.disabled">Is Disabled: {{sortableOptions.disabled}}</button>
JS
app.controller('MainCtrl', function($scope) {
$scope.items = ["One", "Two", "Three"];
$scope.sortableOptions = {
disabled: true
};
});

Resources