AngularJS ng-click does not work in ng-grid - angularjs

I am creating an ASP.net MVC application and in that I am displaying ng-grid with a column which has link as cell content.
Here's how I achieved it.
Controller (ng-grid):
columnDefs: [
{ field: 'FreightRules', visible: true,
cellTemplate:
'<div class="rulesDirective" freight-rules="{{row.entity.FreightItemGuid}}">
</div>',
enableCellEdit: false, enableCellSelection: false }]
Directive:
directives.directive('rulesDirective', function () {
return {
restrict: 'C',
replace: true,
transclude: true,
scope: { freightRules: '#freightRules' },
template: '<div ng-switch on="freightRules | areRulesAvailable">' +
'<div ng-switch-when = true><a ng-click="getRulesTest(\'{{freightRules}}\')" href="#addFreightRuleModal">Rules</a></div>' +
'<div ng-switch-when = false>No Rules</div>' +
'<div ng-switch-default class="grid">Error</div>' +
'</div>'
}});
Filter:
filters.filter('areRulesAvailable', function () {
return function (value) {
if (value != '00000000-0000-0000-0000-000000000000') {
result= true;
}
else {
result = false;
}
return result;
};
});
It works fine but when I click on the Rules link I am not able to display the dialog.
I tried to open the dialog by clicking on a button separately and it worked. Need help!

Since your directive has an isolated scope (see Isolating the Scope of a Directive), which I can tell because its declaration contains scope: {...}, the directive will limit the items on the scope to only what your directive defines.
It is possible for directives to inherit items from a parent scope, or even use the same scope as the parent, but it won't in this case.
When you have an isolated scope, the only way to define items on that scope are to place them there in a controller, link function, or define them as a scope option within your directive. Now, if getRulesTest() is specific only to your directive (not reusable outside of it, not defined on a parent controller, etc.), then just define it in your link function (or directive's controller if it has one, whatever makes sense):
directives.directive('rulesDirective', function () {
return {
restrict: 'C',
replace: true,
transclude: true,
scope: { freightRules: '#freightRules' },
link: function($scope) { // looky here
$scope.getRulesTest = function() {
console.log('check me out');
};
},
template: '<div ng-switch on="freightRules | areRulesAvailable">' +
'<div ng-switch-when = true><a ng-click="getRulesTest(freightRules)" href="#addFreightRuleModal">Rules</a></div>' +
'<div ng-switch-when = false>No Rules</div>' +
'<div ng-switch-default class="grid">Error</div>' +
'</div>'
}});
Since you never define it within the directive's scope anywhere in your example, I presume you are declaring it on a parent controller and you are thinking it should just call out of the directive's scope and find it on the parent scope. As I said above, this won't work. You can use a scope option that binds to a function using the & syntax. This lets your view declaratively assign a function from the parent scope for your directive to use when it needs to call out.
directives.directive('rulesDirective', function() {
return {
restrict: 'C',
replace: true,
transclude: true,
scope: {
freightRules: '=',
getRulesTest: '&rulesTest' // <- this guy. the text on the left is what you want the item named on the scope locally. the text on the right, after the & is what the attribute on the element should be called. if you want them to be the same on both sides, simply enter '&'
},
template: '<div ng-switch on="freightRules | areRulesAvailable">' +
'<div ng-switch-when="true"><a ng-click="getRulesTest({rules: freightRules})" href="">Rules</a></div>' +
'<div ng-switch-when="false">No Rules</div>' +
'<div ng-switch-default class="grid">Error</div>' +
'</div>'
};
})
We added the scope option to your scope declaration. Now your parent controller would add a function on the scope to be bound to:
.controller('MyCtrl', function($scope) {
$scope.doSomethingWithRules = function(rules) {
console.log('sup?');
};
});
And the view would look like:
<div class="rules-directive" rules-test="doSomethingWithRules(rules)"></div>
The other thing that might be problematic is whatever you're doing with this line in the template:
ng-click="getRulesTest(\'{{freightRules}}\')"
Are you trying to pass that function the evaluated string result of {{freightRules}} (which doesn't seem very useful), or do you want to pass it in the actual freightRules object (which seems more likely)? If so, just change it to:
ng-click="getRulesTest({rules: freightRules})"
Edit
Here is a plunk with a working example.
I did catch a small error in my original answer on how to call function scope options. There are a couple of ways to call functions that you have bound, depending on whether or not you have parameters and how you want them matched. This part is a little confusing. If you're not following, just stick to option #2.
1. You want to just pass the name of the function it should call, with parameters determined ordinally.
Say you want them to simply state (note that we only supply the name of the rules, no parenthesis or parameter names):
<div class="rules-directive" ... rules-test="doSomethingWithRules">
Then your directive should invoke the function like this (note that we call a bare function, which generates an invoker that can be used to actually call the function, and then we call it with the actual parameters):
ng-click="getRulesTest()(rules)"
2. You want the invoker to pass in named arguments so you can change the order of or omit parameters.
This is best illustrated if you have multiple parameters. Say the directive supplies two parameters, paramA and paramB. You invoke the function in your directive using an object containing parameter names and values.
ng-click="getRulesTest({paramA: valuesForA, paramB: valuesForB })"
Then when using the directive, your markup looks like this:
<div class="rules-directive" ... rules-test="doSomethingWithRules(paramA, paramB)">
The reason this is preferable is in the situation where you want to reorder or omit a parameter in the function being invoked. Let's say doSomethingWithRules only needs paramB. Now I can do:
<div class="rules-directive" ... rules-test="doSomethingWithRules(paramB)">
I couldn't have done that with Option #1 above, because it always invokes with all of the parameters ordinally. So, Option #2 is preferable because it gives consumers of your directive more flexibility and saves them the hassle of having to remember what order the parameters are supplied to your function.

Related

Accessing ng-repeat scope on a custom directive

I'm having a go at a directive which will dynamically load a template based on a scope value passed into it.
I am using ng-repeat on my directive, and am using the iterated item as an attribute property:
<my-form-field ng-repeat="field in customFields" field="field">
In my directive I have the following code to set the template being used.
(function() {
'use strict';
angular
.module('app')
.directive('myFormField', myFormField);
function myFormField() {
var directive = {
restrict: 'E',
scope: {
field: '='
},
link: function(scope){
scope.getContentUrl = function() {
return 'app/modules/form_components/form_field/' + scope.field.type + '-template.html';
}
},
template: '<div ng-include="getContentUrl()"></div>'
};
return directive;
}
})();
Whilst the above works (which I found from other posts), I wonder if there is a better way.
For example I have seen examples of calling a function on the templateUrl config option instead, and then in that function access the scope attributes being passed in. When I tried this way, my field attribute was a literal 'field' string value (they are objects in my customFields array), so I think at that point the scope variables had not yet been evaluated.
With this current solution I am using, all of my templates get wrapped in an extra div since I am using ng-include, so I am just trying to make the rendered markup more succinct.
Any suggestions\advice is appreciated.
Thanks

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

How can I avoid race conditions at startup when directive attributes are being interpolated?

This fiddle should make things more clear, but essentially I am assigning some attributes of an element (like its id) parameters in its directive:
myApp.directive('myDiv', function () {
return {
restrict: 'E',
replace: true,
scope: {
'elementId': '#',
'displayName': '#',
},
transclude: true,
template: '<div class="my-div" id="{{elementId}}">{{displayName}}<div ng-transclude></div></div>'
}
})
The problem is if I do things immediately at startup, like initialize other directives, those values (e.g. elementId) are not yet interpolated.
In other words, if I get a reference to a myDiv element and print its id, immediately at startup, '{{elementId}}' is printed. But if I wait a short time (say, one second), then the value that was passed as the value to the element-id attribute is printed (as I'd expect).
If you open the console while you view the Fiddle, you'll see that.
What am I doing wrong here? How can I avoid this (other than a lot of really ugly timeouts at startup)?
You have several things incorrect. Here's a new fiddle where you can see the 'at first' log showing the correct value: http://jsfiddle.net/0mq2xv8m/
1) You should include the inner element in the template of the second. You have transclude set to true, so it's going to replace your node. This also ensures that the second directive is not going to bind before the first one is ready. I.e. since it is in the DOM alongside the outter directive, it may instantiate out of step with the wrapping directive.
template: '<div class="my-div" id="elementId">{{displayName}}<div my-field></div></div>'
2) do id="elementId" instead of id="{{elementId}}" to pass by reference instead of value
3) typically it's bad practice to fish for attributes on a parent, it's better to pass it in with 2-way binding. This holds true in any display-list oriented programming I've worked with.
Good Practice:
You should use a controller or a link function for any "init" steps. These wont run until the directive has all its attributes/scope linked. The way you have it, it is executing during the evaluation step during $scope creation (there is no scope yet). Link functions and controllers wait for $scope to become available. A controller can be used instead of a link function (I think it is clearer and easier to unit test).
angular.module('App').controller('someController',[], function() {
var controller = {
init:function(){
console.log(elementId);
}
}
controller.init();
return controller;
});
myApp.directive('myDiv', function () {
return {
restrict: 'E',
replace: true,
controller:'someController',
scope: {
'elementId': '#',
'displayName': '#',
},
transclude: true,
template: '<div class="my-div" id="{{elementId}}">{{displayName}}<div ng-transclude></div></div>'
}

Access repeated item inside ng-repeated directive

I'm building a custom directive in AngularJS that I need to be repeated a couple of times. Currently, my page looks like this:
<div my-item ng-repeat="item in items" />
And my directive looks like this:
module.directive('myItem', function() {
return {
restrict: 'A',
replace: true,
scope: { item: '&' },
template: '<div id="item{{$index}}"></div>',
link: function($scope, element, attributes) {
element.append('<div>' + $scope.item.name + '</div>');
}
};
});
However, inside the linking function, $scope.item.name yields undefined. I'm wondering if there is any way I could access the repeated item inside my directive.
If not, what would be my alternatives? Move the ng-repeat inside the directive, maybe?
P.S. I know that you should (generally speaking) not do DOM manipulation this way, but since I might have ~2000 items that would result in 6000 bindings, and I'm afraid that would lead to severe performance issues.
You should pass item as attribute to directive created a sample directive
http://plnkr.co/edit/l5r6zIc7ncT1XldRuB98?p=preview

How do I call a function to change a value in a directive?

I need something like this, but it doesn't work
.directive('dateTime', function(){
return {
restrict: 'E',
replace: true,
scope:'=value',
template: "<div>{{value.format('mmm dd yy')}}"</div>",
// ^here: applying a function to the scope
};
});
You've created an isolate scope with scope: '=value' so this is a brand new scope that does not prototypically inherit from the parent scope. This means that any functions you want to call must be from
a service, filter, etc. you injected into the directive
another directive's controller, use require to get access (see the tabs and pane directives on the Angular home page for an example)
a function you define in the directive's controller or in the link function on $scope (which are essentially the same thing) Example: https://stackoverflow.com/a/14621193/215945
use the '&' syntax to enable the directive to call a function declared on the parent scope. Example: What is the best way to implement a button that needs to be disabled while submitting in AngularJS?
You might just be looking for the date filter:
{{value | date:'MMM dd yy'}}
but you can do this too:
app.directive('dateTime', function(){
return {
restrict: 'E',
replace: true,
scope:'=value',
template: "<div>{{value | date:'MMM dd yy')}}"</div>",
// ^here: applying a function to the scope
};
});

Resources