I have broken this problem down into it's simplest form. Basically I have a directive that, for the demo, doesn't yet really do anything. I have a div with the directive as an attribute. The values within the div, which come from an object array, are not displayed. If I remove the directive from the div, they are displayed OK. I am clearly missing something really obvious here as I have done this before without any problems.
Here's the Plunk: http://plnkr.co/edit/ZUXD4qW5hXvB7y9RG6sB?p=preview
Script:
app.controller('MainCtrl', function($scope) {
$scope.tooltips = [{"id":1,"warn":true},{"id":2,"warn":false},{"id":3,"warn":true},{"id":4,"warn":true}];
});
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
}
};
});
HTML
<div ng-repeat="tip in tooltips" class="titlecell" cm-tooltip="true">
A div element: {{ tip.id }}
</div>
<br><br>
Just to prove it works without the directive:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.id }}
</div>
There is a hack to make it working in earlier versions of angular by making use of transclusion, like that:
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
transclude: true,
template : '<div ng-transclude></div>'
};
});
PLNKR
As by Beyers' comment above and below, the behaviour the question is about no longer exists in at least 1.2.5
To be clearer; this has nothing to do with ng-repeat, you can remove it and there still will be no tip ( or tooltips ).
See this question on what the = and other configs mean and what it is doing for you.
Basically for your situation when you use = the scope of the directive will be used in the underlying elements, you no longer have your controller's scope. What this means for you is that there is no {{ tip.id }} or not even tip. Because the directive doesn't supply one.
Here's a plunker that demonstrates what you can do with it.
Basically all i did was
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
link: function($scope){ // <<
$scope.tip = { id: 1 }; // <<
} // <<
};
});
This creates the tip object on the scope so it has an id.
For your situation you would probably just not use = and look at this question for your other options depending on what you want.
In my opinion this isn't the way to go.
I would use Objects.
JS code:
function tooltip(id,warn){
this.id = id;
this.warn = warn;
}
tooltip.prototype.toString = function toolToString(){
return "I'm a tooltip, my id = "+this.id+" and my warn value = "+this.warn;
}
$scope.tooltips = [new tooltip(1,true),new tooltip(2,false),new tooltip(3,true),new tooltip(4,true)];
HTML:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.toString() }}
</div>
Related
For some reason when using this function('testclickfn') as ng-click on dynamic elements, it doesn't invoke the function. Here is the angularjs file:
app.controller('testctrl',function($scope){
testfn($scope);
$scope.showelements = function(){
displayTestRows();
}
});
function testfn($scope){
$scope.testclickfn = function(){
alert('testing click fn');
};
}
function displayTestRows(){
for(var i=0; i < 5; i++){
$("#testdiv").append('<p ng-click="testclickfn()">click me</p><br>');
}
}
HTML page that calls angularjs controller 'testctrl':
<div id="testdiv" ng-controller="testctrl">
<button ng-click="showelements()">Show dynamic elements</button><br>
</div>
I'm assuming since the 'click me' tags are being generated after angular has loaded the page, it doesn't know of anything after page is generated so ng-click="testclickfn()" doesn't get registered with angularjs.
How do I get around this situation?
You're creating elements in a way angular has no idea about (pretty bad practice), but not to worry, you can let angular know!
Change the controller signature to
controller('testctrl', function($scope, $compile) {
Then run compile the new elements manually to get the ng-click directive activated
$scope.showelements = function(){
displayTestRows();
$compile($("#testdiv").contents())($scope);
}
If you cant tell, having to use jquery selectors inside your controller is bad, you should be using a directive and the link function to attach the element to the scope (ie, what if you have multiple testctrl elements?), but this'll get you running
As promised
The general rules are that no JS should be outside the angular functions, and that DOM manipulation, where appropriate should be handled by angular also.
Example 1: powerful
Have a look
<div ng-controller="ctrl">
<button ng-click="show('#here')">
create
</button>
<div id="here">
I'll create the clickables here.
</div>
</div>
use controllers for things that share stuff between a lot of different things
.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.sharedVariable = 'I am #';
$scope.show = function(where) {
where = $(where).html('');
//lets create a new directive, and even pass it a parameter!
for (var index = 0; index < 5; ++index)
$('<div>', {'test':index}).appendTo(where);
$compile(where.contents())($scope);
};
}])
use directives for non-unique elements that each have their own states
.directive('test', function() {
return {
//these too have their own controllers in case there are things they need to share with different things -inside them-
controller : ['$scope', function($scope) {
$scope.test = function() {
//see, no selectors, the scope already knows the element!
$scope.element.text(
//remember that parent controller? Just because we're in another one doesnt mean we lost the first!
$scope.$parent.sharedVariable +
$scope.index
);
}
}],
//no need to do things by hand, specify what each of these look like
template : '<p>click me</p>',
//the whole "angular way" thing. Basically no code should be outside angular functions.
//"how do I reference anything in the DOM, then?"; that's what the `link` is for: give the controller access using `scope`!
link : function(scope, element, attributes) {
//you can assign "ng-click" here, instead of putting it in the template
//not everything in angular has to be HTML
scope.element = $(element).click(scope.test);
//did you know you can accept parameters?
scope.index = Number.parseInt(attributes.test) + 1;
},
//just some set up, I'll let you look them up
replace : true,
restrict : 'A',
scope : {}
};
})
Example 2: Simple
But that is just a very generic and powerful way of doing things. It all depends on what you need to do. If this very simple example was indeed all you needed to do you can make a very simple, almost-all-html version:
<div ng-controller="ctrl">
<button ng-click="items = [1, 2, 3, 4, 5]">
create
</button>
<p ng-repeat="item in items" ng-click="test($event)">
<span>click me</span>
<span style="display:none">I am #{{item}}</span>
</p>
</div>
.controller('ctrl', ['$scope', function($scope) {
$scope.test = function($event) {
$($event.currentTarget).children().toggle();
};
}])
That's it, works the same almost
I am not sure if I am doing this correctly, but I am trying to convert something was in a controller to a directive because I want to use it multiple times and just change a few values, so instead of making many huge object literals, I will have just one and just change the values passed in. I am trying to bind chartConfig, but it doesn't seem to be working. Am I doing this wrong?
Here is my directive:
app.directive('percentageSquare', function(){
return {
restrict: 'E',
scope: {
bgClass: '#',
percentage: '#',
chartConfig: '='
},
link: function(scope){
var fontSize = 80;
var percentage = scope.percentage || 0;
scope.chartConfig = {
options: {
chart: {
// Chart settings here
}
}
};
},
templateUrl: '/templates/dashboard/charts/PercentageChart.html'
};
});
Here is the template that the directive is using (PercentageChart.html):
<div class="drop-shadow">
<div class="row">
<div class="col-sm-12">
<highchart config="chartConfig" class="{{bgClass||''}}" ng-show="true"></highchart>
</div>
</div>
</div>
Here is how I am calling the directive:
<percentage-square bg-class="bg-orange" percentage="23"></percentage-square>
Now my chartConfig no longer binds to the directive like it used to when it was in a controller. What can be done to fix this?
Edit
I have gotten a little further, this seems to work:
scope.$watch(scope.chartConfig, function(){
scope.chartConfig = {
// Chart Settings
};
});
But it seems to load the chart twice, as I get two animations.
Looks like what I want to do is watch chartConfig. it must also be wrapped in a string in order for it to work properly. Using scope.chartConfig loaded the chart twice, while 'chartConfig' loads the chart once and properly.
scope.$watch('chartConfig', function(){
scope.chartConfig = {
// Chart Settings
};
});
Just getting my head around Angular - failing to understand a few concepts as I come from the Backbone school of thought.
I've picked a random project to get started: a card game.
Let's say that I wanted to define a hand controller and a card controller. For simplicity, I want to have them as directives.
Here is the card directive:
app.directive('card', function(){
return {
restrict:'E',
templateUrl:'card.html',
controller:function($scope){
this.suit = 'clubs';
this.rank = 'a';
this.suitClass = function(){
return this.suit + '-' + this.rank;
}
},
controllerAs:'card'
};
});
And here is the hand directive:
app.directive('hand', function(){
return {
restrict:'E',
template:'hand.html',
controller:function($scope){
this.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
},
controllerAs:'hand'
}
});
With the following plunker, I was expecting to be able to simply drop in the <hand></hand> element and have angular do all the work for me. In my minds eye there should be cards representing different suits nested within the <hand> directive. What am I missing? Currently, as you can tell in the plunker, the nested controller/directive does not instantiate the view properly.
Am I thinking in too much of an MVC way? Is OOP haunting me? Or is angular just badly designed?
I am not 100% sure that I understand your question but I think that this is a better way to write it:
var app = angular.module('app', []);
app.directive('card', function(){
return {
restrict:'E',
templateUrl:'card.html',
replace: true,
link: function ($scope, element, attrs){
$scope.suit = 'clubs';
$scope.rank = 'a';
$scope.suitClass = function(){
return this.suit + '-' + this.rank;
}
}
};
});
app.directive('hand', function($compile){
return {
restrict:'E',
templateUrl:'hand.html',
link:function($scope, element, attrs){
$scope.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
}
}
});
And the html can be something like these:
(hand directive template)
<div>
<card ng-repeat="card in cards"></card>
</div>
And (card directive template)
<div ng-class="card.suitClass()">
{{ suit }}
</div>
I will explain the problem by going top down through the order of elements/objects that will be called:
hand directive:
The directive is ok so far. But the $compile parameter and the $scope parameter are not used an should be removed. To be more clear I applied this to a variable hand, but it does not change the behaviour of the application.
app.directive('hand', function(){
return {
restrict:'E',
templateUrl:'hand.html',
controller:function() {
var hand = this;
hand.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
},
controllerAs:'hand'
}
});
hand.html:
You never passed the current card of the ng-repeat to the card directive.
That way you only produce the card templates times the number of card but never using the actual values.
I removed the obsolete div tag and enhanced the hand.html to this:
<card ng-repeat="card in hand.cards" card-model="card"></card>
This way I get every card from the hand view in the card directive.
card directive:
First I remove the $scope variable because it is never used and won't be used here.
This function is rather incomplete. At least it is missing the card values you want to use. But a major problem in here is that the context of this is bound to the caller. To be more precise, you are using this inside of the suitClass function, but you want to use the suit and rank values of the controller. this does not point to the controller function but to the newly created suitClass function which doesn't know any of these values. For that problem you should introduce a variable that holds the context and access the values that way. And I add the scope variable cardModel that is bound to the element attribute to get the desired values. And I add the bindToController: true to access the passed in model as card.cardModel instead of the pure cardModel:
app.directive('card', function(){
return {
restrict:'E',
scope: {
cardModel: '='
},
templateUrl:'card.html',
controller:function(){
var card = this;
console.log(card.cardModel)
card.suitClass = function(){
return card.cardModel.suit + '-' + card.cardModel.rank;
}
},
controllerAs:'card',
bindToController: true
};
});
card.html:
This view is okay. I only applied my changes:
<div ng-class="card.suitClass()">{{ card.cardModel.rank }}</div>
I hope it is still useful for anybody.
I'm just learning angular and creating some simple directives to try some things. I am having (what I think) is a small problem attempting to pass parameters from the directive to a controller function on the root scope.
Please see the following jsfiddle and note that I clicking the button (from within the directive) gives me undefined whereas it seems to work fine if clicking the button from the controller itself.
jsfiddle
Am I just missing something syntax wise? Or am I completely wrong in how this should work? I have made several attempts at placing variables in different locations (note the 'xxx') in the fiddle to see if anything would work and I get either errors or nothing.
<div ng-app="myApp" ng-controller="myController">
<!-- root scope -->
<div style="background-color: teal">
<button ng-click="propertyF('yyy')" >F</button>
</div>
<!-- directive firing methods on the root scope -->
<div style="background-color: coral">
<my-directive3 property6="propertyF()"></my-directive3>
</div>
</div>
var app = angular
.module('myApp', [])
.controller('myController', [
'$scope', function($scope) {
$scope.propertyF = function (aValue) {
alert("propertyF fired: '" + aValue + "'");
};
}
])
.directive('myDirective3', function() {
var directive = {
link : function link(scope, element, attrs) {
console.log("link directive 3");
},
restrict : 'EA',
replace : true,
scope : {
property6: '&'
},
template: '<button ng-click="property6(\'xxx\')">property6</button>'
};
return directive;
});
With the way Angular works, are you passing a function binding and specifying arguments. propertyF() does not specify any arguments.
property6="propertyF(arg)"
Then you can do Angular's unique syntax for handling this:
ng-click="property6({arg:\'xxx\'})"
http://jsfiddle.net/ue1trkt9/1/
I am using "draggable" directive to support image dragging. However, as per the role of the user, I need to disable image dragging for certain groups of users. I have used following code.
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-repeat="template in templates" draggable id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
The method dragSupported is in the template scope and returns true or false. I don't want to create two big duplicate <li> elements by using ng-if for each value returned by dragSupported(). In other words, I am not looking for the following approach to solve this.
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-if="dragSupported() ==true" ng-repeat="template in templates" draggable id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
<!--remove "draggable" directive as user doesn't have permission to drag file -->
<li ng-if="dragSupported() !=true" ng-repeat="template in templates" id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
Is there any other approach to avoid code duplicity?
ng-attr-<attrName>
Support for conditionally declaring an HTML attribute is included with Angular as the dynamically-titled ng-attr-<attrName> directive.
Official Docs for ng-attr
Example
In your case, the code might look like this:
<li
id="{{template._id}}"
class="template-box"
type="template"
ng-repeat="template in templates"
ng-attr-draggable="dragSupported() === true"
></li>
Demo
JSFiddle
This contains examples of usage for the following values: true, false, undefined, null, 1, 0, and "". Note how typically-falsey values may yield unexpected results.
Thanks Jason for your suggestion. I took little different approach here. Since I don't want to change the "scope" variable therefore I used "attrs" to check if drag is allowed or not. Following is approach I tool which seems good so far.
Directive code:
app.directive('draggable', function () {
return {
// A = attribute, E = Element, C = Class and M = HTML Comment
restrict: 'A',
replace:true,
link: function (scope, element, attrs) {
if(attrs.allowdrag =="true")
{
element.draggable({
cursor: 'move',
helper: 'clone',
class:'drag-file'
});
}
}
}
});
HTML Code:
<ul>
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-repeat="template in templates" draggable allowdrag="{{userHasPrivilege()}}" >
<!--Ohter code part of li tag-->
</li>
</ul>
Controller is having implementation of userHasPrivilege().
Not sure if this is correct way or not. Looking for thoughts.
There is no way to directly add or remove an attribute from an element. However, you could create a directive that simply adds the attribute to the element when the condition is met. I've put something together that illustrates the approach.
Demo: http://jsfiddle.net/VQfcP/31/
Directive
myApp.directive('myDirective', function () {
return {
restrict: 'A',
scope: {
canDrag: '&'
},
link: function (scope, el, attrs, controller) {
/*
$parent.$index is ugly, and it's due to the fact that the ng-repeat is being evaluated
first, and then the directive is being applied to the result of the current iteration
of the repeater. You may be able to clean this by transcluding the repeat into the
directive, but that may be an inappropriate separation of concerns.
You will need to figure out the best way to handle this, if you want to use this approach.
*/
if (scope.canDrag&& scope.canDrag({idx: scope.$parent.$index})) {
angular.element(el).attr("draggable", "draggable");
}
}
};
});
HTML
<ul>
<!-- same deal with $parent -->
<li ng-repeat="x in [1, 2, 3, 4, 5]" my-directive="true" can-drag="checkPermissions(idx)">{{$parent.x}}</li>
</ul>
Controller
function Ctl($scope) {
$scope.checkPermissions = function(idx) {
// do whatever you need to check permissions
// return true to add the attribute
}
}
I used a different approach as the previous examples didn't work for me. Perhaps it has to do with using custom directives? Perhaps someone can clear that up.
In my particular example, I'm using ui-grid, but not all ui-grids should use pagination. I pass in a "paginated" attribute and then $compile the directive based on true/false. Seems pretty brutish but hopefully it can push people in a positive direction.
HTML
<sync-grid service="demand" paginated="true"></sync-grid>
Directive
angular
.module('app.directives')
.directive('syncGrid', ['$compile', SyncGrid]);
function SyncGrid($compile){
var nonPaginatedTemplate = '' +
'<div>' +
' <div ui-grid="gridOptions" class="grid"></div>' +
'</div>';
var paginatedTemplate = '' +
'<div>' +
' <div ui-grid="gridOptions" class="grid" ui-grid-pagination></div>' +
'</div>';
return {
link: link,
restrict: 'E',
replace: true
};
function link(scope, element, attrs) {
var isPaginated = attrs['paginated'];
var template = isPaginated ? paginatedTemplate : nonPaginatedTemplate;
var linkFn = $compile(template);
var content = linkFn(scope);
element.append(content);
// Continue with ui-grid initialization code
// ...
}
}