Adding a CSS class to element on ng-click - angularjs

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:
...

Related

How to get values resolved in template without double curly brackets

I am using directive and template. My template is as follows
<div>
<a data-my-dir="item" data-attr1="true"
data-attr2="{{itemParentId}}"
data-attr3="{{item.id}}">
</a>
</div>
Here due to curly braces watches are added and it is affecting my performance. I don't want watches as I am not going to modify attr2 or attr3.
I want to directly resolved value here only.
We can use bindonce directive where we don't want watches where we can use bo-text="xyz" instead, but here I want to pass values as attr to my custom directive.
Inside my directive's link function I am binding click event with element as follows
link: function (scope, element, attrs) {
element.bind('click', function (event) {
var myAttr1 = attrs.attr1;
var myAttr2 = attrs.attr2;
}
}
So due to those watches in template on attr1 and attr2 I am getting these values resolved in click event.
What are the alternatives?
One time Binding
Seems like a good use case for one time binding (if you are using angular 1.3+)
<div>
<a data-my-dir="item"
data-attr1="true"
data-attr2="{{::itemParentId}}"
data-attr3="{{::item.id}}">
</a>
</div>
the directive would look like
angular.module('app', [])
.directive("myDir", function() {
return {
restrict: "A",
scope: {
"attr1": "#",
"attr2": "#",
"attr3": "#",
},
template: '<span> {{attr1}} {{attr2}} {{attr3}}</span>'
};
})
Demo
http://plnkr.co/edit/GJCZmb9CTknZZbcRHN7s?p=preview
You could use data-attr2="itemParentId" directly but for that you need to use = as currently you are using # option of directive.
app.directive('myDir', function(){
return {
scope: {
dataAttr1: '=', //or '=dataAttr1'
dataAttr2: '=' //or '=dataAttr2'
},
link: function(scope, element, attrs){
console.log(scope.dataAttr1);
console.log(scope.dataAttr2);
}
}
})

AngularJS directive: Produce different HTML depending on $scope variable

I just started using AngularJS and immediately ran into a problem:
I have a sidebar which contains "action-buttons" - depending on the currently active view, different buttons should be visible.
My view-controller defines an object which looks as follows:
$scope.sidebar.actionButtons = [
{ icon: "plus", label: "Add", enabled: true, url: "customer.new" },
{ icon: "minus", label: "Delete", enabled: false, action: function(){ alert("Not implemented yet"); }}
];
As you can see, there are two different kinds of action-buttons: Either the button changes to another view (url is set to customer.new), or the button triggers an arbitrary function (action is set to alert()).
Each button type has to generate some slightly different html, and I'm not able to get this working.
After playing around for several hours, here is my current (not-working) approach:
My sidebar uses the following template-code to generate the buttons:
<ul class="nav" id="sidebar-action-buttons">
<action-button ng-repeat="button in actionButtons" button="button"/>
</ul>
Now, the actionButton directive has everything it needs and should produce the html depending on the button type:
angular.module('myApp')
.directive('actionButton', function($compile) {
function linker($scope, $element, $attrs){
var innerHtml = '';
$element.attr('ng-class', '{disabled: !button.enabled}');
if($scope.button.url) {
$element.attr('ui-sref-active', 'active')
innerHtml = '<a ui-sref="{{button.url}}">';
} else {
innerHtml = '<a ng-click="button.action()">';
}
innerHtml += '{{button.label}}</a>';
$element.html(innerHtml).show();
$compile($element.contents())($scope);
}
return {
restrict: 'E',
replace: true,
scope: { button: "=" },
link: linker,
template: "<li></li>"
}
});
This generates the correct content. The problem here is, that the attributes which are placed on the actionButton element (in this case ng-class='{disabled: !button.enabled}') are not compiled.
How can a directive produce different html depending on scope variables? What is the correct approach for doing this? How can I also compile the newly added attributes?
By the time the ng-class is added to the action-button element, the digest is over with for that element. You could call $scope.$apply(), but I would add the ng-class to each anchor element instead, then there would be no need to call $scope.$apply() again.
Because you are compiling content() of the li but ng-class has been added with li itself. Simple solution is to add ng-class directly with the action-button directive i.e.
<action-button ng-repeat="button in actionButtons" button="button" ng-class="{disabled: !button.enabled}" />

Add directive in loop based on criteria

I have a timeline.json like:
[{
"from":"twitter",
//...
},
{
"from": "facebook"
//...
}]
Based on from attribute I need "render" specific directives: post-twitter or post-facebook Inside a loop
How could I do that?
There are several ways. You can use ng-switch, ng-if, or a custom handler function in the link phase. You can find more info in the documentation.
Basically, switch between tags that invoke the directive based on a conditional statement, being it the 'from' value in your model. You can combine for example the ng-if directive with ng-include, to render a portion of your template based on this condition. Just make sure it won't degrade the performance.
You can create a directive which compiles wanted directive depending on the passed argument.
Something like:
.directive('renderOther', function($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: functtion(scope, elem, attrs) {
var dir = $('<post-' + scope.type + '></post-'+scope.type+'>');
dir.appendTo(elem);
$compile(elem.contents())(scope);
}
}
});
Html:
<div ng-repeat="i in items">
<render-other type="i.from"></render-other>
</div>
(Its not working code - just an idea)

How to re-render a directive template and link function?

I am creating a navigation "tree" that basically allows you to navigate to parent views like so
Home > Planner > Contacts > john.smith#hotmail.com
This list is just a normal <ul> that should get changed each time the view is changed and allow the user to click on any of the above links to navigate. I am using ui-router's $stateChangeStart event to build up my list of links, the problem is that my directive does not "re-build" this list every time the page change.
Is there a way to force the directive to re-render every time I change my state?
TreeNav directive
app.directive('treeNav', ['$rootScope', function ($rootScope) {
return {
replace: true,
restrict: 'E',
template: '<ul>\
<li ng-repeat="item in treeNavs">\
<a ui-sref="{{ item.state }}">{{ item.name }}</a>\
<span> > </span>\
</li>\
</ul>',
link: function (scope, elem, attrs) {
scope.treeNavs = $rootScope.treeNav.reverse();
scope.$watch('$rootScope.treeNav', function (newVal, oldVal) {
scope.treeNavs = newVal.reverse();
});
}
}
}]);
I believe you aren't setting up your $watch statement correctly.
You should also be careful with your use of .reverse. reverse() reverses the entries in an array in place, which is probably not what you want to do, since it would modify the array on the $rootScope.
You can use splice() to create a new copy and then reverse that:
scope.treeNavs = $rootScope.treeNavs.splice().reverse();
$rootScope.$watch('treeNav', function(newVal) {
scope.treeNavs = newVal.splice().reverse();
});

AngularJS directive only when the condition is true

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

Resources