How to pass methods with pre defined argument to Angular JS Directives - angularjs

I am trying to add an Action Buttons to my DataTable depending if the status is true by passing a method to the directive.
CONTROLLER.JS
_ctr.route = 'home.stocks';
_ctr.object = [
{
id: 1,
name: 'XXXX',
status: true
}, {
id: 2,
name: 'XXXX',
status: false
}
];
_ctr.headers = {
name: 'NAME',
status: 'STATUS'
}
_ctr.method = row => {
if (row.type) {
return row['button'] = [
{
label: 'Edit',
class: 'primary',
state: _ctr.route + `.edit({ id : '${row.id}' })`
}, {
label: 'Delete',
class: 'danger',
state: _ctr.route + `.delete({ id : '${row.id}' })`
}
];
}
}
This is how I transfer variables to the Directive.
FORM.HTML
<template-table
tb-object="{{ _ctr.object }}"
tb-headers="{{ _ctr.headers }}"
tb-method="{{ _ctr.method() }}">
</template-table>
This is my customized Table.
TEMPLATE.JS
app.directive('templateTable', function (factory, $compile) {
return {
restrict: 'E',
replace: true,
scope: {
tbMethod: '#',
tbObject: '#',
tbHeaders: '#',
},
link: (scope, element, attrs) => {
attrs.$observe('tbObject', data => {
if (data) {
let object = scope.$eval(data);
let headers = scope.$eval(scope.tbHeaders);
let method = scope.tbMethod;
let headers_config = [];
// converting `headers` to DataTable Column Format
for (let prop in headers) {
headers_conf.push({
data: prop,
bSortable: true,
label: headers[prop]
});
}
if (method) {
headers_conf.push({
mData: null,
bSortable: false,
label: 'ACTION',
mRender: function (row, type, full) {
// calls method and passes `row` argument to method
// checks if `row.type` is `true`
row[method(row)];
let button = row.button;
let template = '';
// if true appends 'Action Button'
if (button) {
button.map(res => {
template +=
`<a ui-sref="${res.state}" class="btn btn-${res.class}
mr-2" style="color: white;" role="button"> ${res.label}
</a>`;
})
}
return `<td class="action-buttons"> ${template} </td>`;
}
});
}
scope.headers = headers_conf;
$("#listTable").DataTable({
data: object,
columns: headers_conf,
"fnDrawCallback": function (oSettings) {
$compile(angular.element($("#listTable")).contents())
(scope);
}
});
}
});
}
}
});
This doesn't seem to work for me. What should I do to make the code above, work.

To pass a method to directive with isolate scope, don't use curly brackets:
<template-table
tb-method="_ctr.method">
</template-table>
In the isolate scope directive, use one-way binding:
app.directive('templateTable', function (factory, $compile) {
return {
restrict: 'E',
scope: {
tbMethod: '<',
},
link: (scope, element, attrs) => {
if (scope.tbMethod) {
var row = "something";
//Invoke method
scope.tbMethod(row);
};
}
}
})
From the Docs:
scope
The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
For more information, see
< or <attr - set up a one-way (one-directional) binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name.
AngularJS Comprehensive API Reference - scope

Related

How can I apply my custom validation on a ui-grid cell template

Is there a way to bind custom validation. I would like to bind a method on say ng-keydown to check the input against my set of rules.. How can that be done. I trying calling a $scope function on ng-change and that didn't work.
I tried this ng-change="grid.appScope.checkValidaton($event,MODEL_COL_FIE‌​LD,true,true). The scope function gets called however the arguments are undefined. How can I pass the $event and ng-model along.
And this is my column:
{ name: "group", editableCellTemplate:
"<div><input type=\"INPUT_TYPE\" ng-class=\"'colt' + col.uid\" ui-grid-editor ng-model=\"MODEL_COL_FIELD\" ng-change=\"grid.appScope.checkValidaton($event,MODEL_COL_FIELD,true,true)\"></div>", displayName: "Group", enableCellEdit: true, showSortMenu: false, cellTooltip: true
},
I had my reference from : http://plnkr.co/edit/4Pvc4UYKSf71pIC2XrpY?p=preview
After a while of searching over internet and reading about ui-grid events, I coded a directive to use the scope entity and ui-grid events to apply validation on click of cell.
The basic trick was to use the custom editable template and apply validation on its field.
Here is the code, it can also be found at my repository here
(function(module){
module.directive('gridValidation', gridValidationFn);
gridValidationFn.$inject = ['uiGridEditConstants'];
function gridValidationFn(uiGridEditConstants) {
var directive = {
restrict: 'A',
template: '<div><input type="text" ng-model="txtValue" ng-change="changeTxt(txtValue)"/></div>',
scope: true,
link: gridValidationLinkFn
};
return directive;
function gridValidationLinkFn(scope, elm, attr) {
var oldTxt = scope.row.entity[scope.col.field];
scope.limit = attr.limit;
scope.txtValue = oldTxt;
function windowClickListener(ev) {
if (ev.target.nodeName !== "INPUT")
scope.editDone();
}
scope.changeTxt = function (val) {
if (attr.words) {
if (scope.txtValue && scope.txtValue.match(/\S+/g) && scope.txtValue.match(/\S+/g).length > Number(scope.limit)) {
scope.txtValue = scope.txtValue.split(/\s+/, Number(scope.limit)).join(" ");
scope.row.entity[scope.col.field] = scope.txtValue.split(/\s+/, Number(scope.limit)).join(" ");
}
else
scope.row.entity[scope.col.field] = scope.txtValue;
}
else {
if (scope.txtValue && scope.txtValue.length > Number(scope.limit)) {
scope.txtValue = scope.txtValue.slice(0, Number(scope.limit));
scope.row.entity[scope.col.field] = scope.txtValue;
}
else
scope.row.entity[scope.col.field] = scope.txtValue;
}
};
scope.editDone = function () {
scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
};
scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
angular.element(window).on('click', windowClickListener);
});
scope.$on('$destroy', function () {
angular.element(window).off('click', windowClickListener);
});
}
}
}(angular.module("ModuleName")));
It works nicely for me. Hope it helps someone else

Kendo-ui angular directive on editor

I'm trying to attach an angular directive to `
{
field:"stateID",
hidden: true,
title: "State",
editor: function (container, options) {
var _statesDirective = $('<div><my-states></my-states></div>');
_statesDirective.appendTo(container);
}`
My diretive looks like this:
appRoot.directive('myStates', function () {
return {
restrict: 'EA',
templateUrl: 'directivesHTML/ddStates.html',
scope:false,
controller: function ($scope)
{
var dsStates = new kendo.data.DataSource({
autoBind: false,
page: 1,
transport: {
read: {
url: "api/util/getStates",
dataType: "json"
}
},
schema: {
model: {
id: "stateID",
fields: {
stateID: { type: 'string' },
name: { type: "string" }
}
}
}
});
dsStates.read().then(function () {
$('#ddStates')
.kendoDropDownList({
dataTextField: "name",
dataValueField: "stateID",
dataSource: dsStates,
optionLabel: '--',
change: function (e) {
}
});
});
}
};
});
For some weird reason, it wont work, if I put the directive someplace else, any outside html page, it works just fine, but not from here. I thought it could be the version, upgraded it to the latest one for this month to no avail.
Any clues ?
-thanks,
You need to compile your html before appending it (using $compile service).
So in your editor: function,
// Specify what it is we'll be compiling.
var to_compile = '<div><my-states></my-states></div>';
// Compile the tag, retrieving the compiled output.
var _statesDirective = $compile(to_compile)($scope);
// Ensure the scope and been signalled to digest our data.
$scope.$digest();
// Append the compiled output to the page.
$compiled.appendTo(_statesDirective);
For more reference, Angular Docs for $compile
Also, how can we use $compile outside a directive in Angularjs

Access controller function from directive to update an HashMap

I was trying to call a controller function from a directive in order to update a counter inside an hash-map.
After reading a few solutions, I ended up doing this:
'use strict';
var dragDropApp = angular.module('dragDropApp', []);
dragDropApp.controller('DragDropCtrl', function($scope) {
$scope.itemCount = {
'item1' : {
'count' : 0
},
'item2' : {
'count' : 0
},
'item3' : {
'count' : 0
}
};
//this.updateItemCounter = function(itemId) {
// $scope.itemCount[itemId].count++;
//}
$scope.updateItemCounter = function(itemId) {
$scope.itemCount[itemId].count++;
}
}
dragDropApp.directive('droppable', function() {
return {
restrict : 'A',
scope : {
drop : '&', // parent
bin : '=' // bi-directional scope
},
controller : 'DragDropCtrl',
link : function(scope, element, attrs, DragDropCtrl) {
var el = element[0];
el.addEventListener('drop', function(e) {
var item = document.getElementById(
e.dataTransfer.getData('Text')).cloneNode(true);
//DragDropCtrl.updateItemCounter(item.id);
>>>> scope.$parent.updateItemCounter(item.id); <<<<
return false;
}, false);
}
}
});
It works and does what I want, but I don't know if this approach is correct. Is it?
I've also tried to use the controller to access the function updateItemCounter, but the hash-map does not get updated, having the same values every time I call the function.
In html you should set attributes like this:
<div droppable bin="something" drop="updateItemCounter(itemId)"></div>
In directive if you want to call your controller's function you should call it like this. Note that property name of passed object must be the same as used in html
link : function(scope, element, attrs, DragDropCtrl) {
...
scope.updateItemCounter({itemId: item.id});
...
}
if you wold like to pass more arguments you should use in html:
<div droppable drop="updateItemCounter(itemId, param2, param3)"></div>
and call:
scope.updateItemCounter({itemId: item.id, param2: 'something', param3: 'something'});
Read these answers:
https://stackoverflow.com/a/27997722/1937797
https://stackoverflow.com/a/23843218/1937797

How to create angularjs custom directive for google location

I'm trying to create a directive to lookup google locations using <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true&libraries=places&language=en-US"></script> like this:
angular.module('MyApp').directive('locationPicker', function () {
return {
restrict: 'AE',
replace: true,
require: 'ngModel',
template: '<input id="{{id}}" type="text" class="{{class}}" placeholder="{{placeholder}}" />',
scope: {
id: '#',
class: '#',
placeholder: '#',
},
link: function ($scope, elm, attrs, controller) {
var autocomplete = new google.maps.places.Autocomplete(elm[0], {types: ['geocode']});
var componentForm = {
locality: 'long_name',
administrative_area_level_1: 'short_name',
country: 'long_name'
};
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var place = autocomplete.getPlace();
var lat = place.geometry.location.lat();
var lng = place.geometry.location.lng();
var name = "";
for (var i = 0; i < place.address_components.length; i++) {
var addressType = place.address_components[i].types[0];
if (componentForm[addressType]) {
if (name !== "") {
name += ", ";
}
var val = place.address_components[i][componentForm[addressType]];
name += val;
}
}
elm[0].value = name;
$scope.$apply(function () {
controller.$setViewValue({name: name, lat: lat, lng: lng});
});
});
}
};
});
And my input:
<input id="location" location-picker ng-model="location" class="form-control" placeholder="Project location" ng-required="true" />
In my controller:
$scope.location = {
name:null,
lat:null,
lng:null
};
Everything looks fine but when my component is first rendered, the value of the input is [Object object] instead of the place holder (Project Location).
What am I doing wrong?
Problem
You are binding ngModel to the location object, which renders as [object Object] when coerced to a string.
Solution
Since you are grabbing hold of the NgModelController in your directive, you can use its $formatters pipeline to transform the model value (location object with name, lat, lng properties) into the view value and the $render function to specify how to render the value if it is changed outside of the ngModel lifecycle.
Here is a working plunker: http://plnkr.co/edit/5HPFelbDFUrzeccGfgYx?p=preview. The crucial piece of code is
// triggered by $setViewValue
controller.$formatters.push(function (value) {
return value ? value.name : value;
});
// triggered by clear() from controller
controller.$render = function () {
input.value = controller.$modelValue.name;
};
I have also made the following modifications to your code:
Used location-picker as an element directive. input is not a good choice of element since it already has bindings to ngModel.
Removed replace: true. It is a deprecated configuration option in directives, and can cause some strange behaviors, since it tries to mix attributes of directive element and attributes of template element.
Removed elm[0].value = name;. This is handled by the lifecycle of the ngModel when you call $setViewValue.

Dynamically generating widgets from json data without performance losses

Here is the problem I'm trying to solve: generating a form from a variable set of widgets where the exact widgets and their ordering is directed by the data, namely schema. The first approach I've taken looks like (omitting the unnecessary details):
controller.js:
angular.module('app').controller(function($scope) {
$scope.data = {
actions: [{
name: 'Action1',
base: 'nova.create_server',
baseInput: {
flavorId: {
title: 'Flavor Id',
type: 'string'
},
imageId: {
title: 'Image Id',
type: 'string'
}
},
input: [''],
output: [{
type: 'string',
value: ''
}, {
type: 'dictionary',
value: {
key1: '',
key2: ''
}
}, {
type: 'list',
value: ['', '']
}]
}]
};
$scope.schema = {
action: [{
name: 'name',
type: 'string',
}, {
name: 'base',
type: 'string',
}, {
name: 'baseInput',
type: 'frozendict',
}, {
name: 'input',
type: 'list',
}, {
name: 'output',
type: 'varlist',
}
]
};
})
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<div ng-repeat="spec in schema.action" ng-class="{'right-column': $even && isAtomic(spec.type), 'left-column': $odd && isAtomic(spec.type)}">
<typed-field></typed-field>
<div class="clearfix" ng-show="$even"></div>
</div>
</collapsible-panel>
directives.js
.directive('typedField', function($http, $templateCache, $compile) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + scope.spec.type + '.html',
{cache: $templateCache}).success(function(templateContent) {
element.replaceWith($compile(templateContent)(scope));
});
}
}
})
Among the templates located inside '/fields/' the simplest possible template for the string-type field is
<div class="form-group">
<label>{$ spec.title || makeTitle(spec.name) $}</label>
<input type="text" class="form-control" ng-model="item[spec.name]">
</div>
This approach works for - all the widgets are rendered, model bindings work, but on once I type a single letter inside of these widgets, the scope changes and the widgets are redrawn, causing:
* focus losing
* some time delay effective meaning poor performance.
Trying to overcome this drawback, I've rewritten my app in the following way:
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<action></action>
</div>
directives.js
.directive('typedField', function($http, $templateCache, idGenerator, $compile) {
return {
restrict: 'E',
scope: true,
compile: function ($element, $attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + $attrs.type + '.html',
{cache: $templateCache}).success(function (templateContent) {
$element.replaceWith(templateContent);
});
return function (scope, element, attrs) {
scope.title = $attrs.title;
scope.type = $attrs.type;
scope.name = $attrs.name;
}
}
}
})
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
return function(scope, element, attrs) {
}
}
}
})
Instead of getting schema from scope, I'm providing it via dependency injection into compile phase of a directive (which is run only the first time - which seemed quite the thing I needed to avoid repetitive full redraw of widgets). But now instead of nicely looking widgets (as before) I get raw html with data bindings not evaluated at all. I guess that I'm doing something wrong, but fail to graps how should I correctly use the compile function to avoid performance issues. Could you please give an advice on what should be fixed?
Finally I have found out what should be returned from the directive's compile function in that case
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
var linkFns = [];
tElement.children().each(function(tElem) {
linkFns.push($compile(tElem));
});
return function(scope) {
linkFns.forEach(function(linkFn) {
linkFn(scope);
});
}
}
}
})
What actually $compile does is calling the 'compile' function of each directive it encounters - thus calling $compile on current directive's template lead to infinite recursion, but calling it for each child of this directive worked fine.

Resources