How to dynamically add in custom directive from other custom directive - angularjs

I've created a directive that has similar functionality to datatables, but it's been customized for our app. One thing I have in my directive scope is columnDefinitions. Each object in that array has a property called data. I've got it set up so that if it is set to a string, it looks for that property on the entity, and it's a function, it will call that function with the entity. So basically this:
scope.getEntityData = function(entity, currColumnDefinitionData) {
var entityData = null;
if (angular.isString(currColumnDefinitionData))
{
entityData = entity[currColumnDefinitionData];
}
else if(angular.isFunction(currColumnDefinitionData))
{
entityData = currColumnDefinitionData(entity);
}
else
{
$log.error("Column defintion data property must be a string or a function. Cannot get entity data.");
}
return entityData;
};
And then in my directive template, something like this:
<tr ng-repeat="currEntity in entities">
<td ng-repeat="currColDef in columnDefinitions">
{{getEntityData(currEntity, currColDef.data)}}
</td>
</tr>
This works great when I just need to output a string. I now have a case where I want it to insert a directive for the data in that column. I first just had the data property equal the HTML string. For example:
data: function(entity) {
return '<div my-directive></div>';
},
However, that resulted in the string just being inserted into the table cell (Angular escaping the text for me)
What I'm wanting to know, is how I can set up my directive so that I can get compiled directives into my table cells. I thought about having some way of telling myself it was a directive, and then compiling it with the $compile service, but then I don't know what to return from my function for it all to work right. Any ideas would be much appreciated.

Here's how I would do it
The directive:
angular.module('ui.directives').directive('uiCompile',
[ '$compile', function(compile) {
return {
restrict : 'A',
link : function(scope, elem, attrs) {
var html = scope.$eval('[' + attrs['uiCompile'] + ']')[0];
elem.html(html);
compile(elem.contents())(scope);
}
}
} ]);
The template:
<tr ng-repeat="currEntity in entities">
<td ng-repeat="currColDef in columnDefinitions" ui-compile="currColDef"></td>
</tr>
Basically for each column definition compile the content as a template using the current scope.

Related

Dynamically inserting directives/components into angular table cells

I have a table directive that basically loops through an array and inserts html like this:
<tr ng-if="isComplete()" ng-repeat="row in paged.page()" ng-click="rowClick(row, $event)">
<td ng-repeat="header in headers" ng-bind-html="trustAsHtml(header.formatter(row[header.property]))"></td>
</tr>
The directive is configured like this:
$scope.headers = [
comsTable.header('id', 'ID', function(id, obj) {
return '' + id + '';
}),
comsTable.header('name', 'Name'),
comsTable.header('age', 'Age')
];
WHere the first argument is an object property of each element in the array.
And ng-bind-html is used and I can pass in html and I use trustAsHtml from the $ce service.
I am able to insert basic html like this when configuring the table:
$scope.headers = [
comsTable.header('id', 'ID', function(id, obj) {
return '' + id + '';
}),
If I wanted to insert a dynamically created directive. Can I achieve this without re-architecting the whole table. I believe I would have to use $compile and attach the node/element to the DOM but could I wait until after the table has been rendered to call $compile or what would be the best way of achieving this?
I'm new to angular so I'm not sure of what is the best way of achieving this.
Here is a plunker that shows the directive I have so far

Angularjs directive creation

I have a set of two functions that I user to perform a lookup to identify a user. In my app I need to do this for multiple roles (i.e: requester, processor, etc.). I think I want to create a directive so that I can reuse the code without having to copy and change it for each role - if I understand the concept of a directive.
I made an attempt but instead of an input field actually showing up for me to enter a name in, I see the name of the template html file. I am obviously doing something wrong, but since I am very new to directives (custom and creating them), I am sure I didn't do it right.
What I want is the ability to type a person's name in, and as I type, it should call a function that is doing the actual search. While a match is being looked for, I want a sliding bar to appear to indicate something is occurring. After I choose the appropriate user, I want the name and id retrieved to be stored in my model and the bar will close.
This is what I put in my HTML to call my directive:
<name-user idModel="request.requesterID" nameModel="request.requester"></name-user>
the requesterId and requester are where I want the results....
My directive:
app.directive('nameUser', function() {
return {
restrict: 'E',
require: {
idModel : '=',
nameModel : '='
},
template : 'app/views/nameUser.html',
link : function($scope){
$scope.searchName = function(searchQuery, bar){
var bar = "#loadingBar" + bar;
if(searchQuery && searchQuery.length >= 3){
$(bar).slideDown();
$http.get("/read/userinfo/" + searchQuery ).success(function(data){
$scope.nameSearchResults = data;
$(bar).slideUp();
});
}
}
$scope.selectName = function(pl){
nameModel.$setViewValue(pl.name);
idModel.$setViewValue(pl.user_id);
$scope.nameSearchResults = {};
}
}
};
});
My template has:
<input type="text" class="form-control input-md" ng-model="nameModel" ng-model-options='{ debounce: 800 }' ng-change="searchName(nameModel,'88')" value="nameModel"/>
<div class="row" style="padding:20px;display:none;" id="loadingBar88">
<div class="col-md-6 col-md-offset-5">
<img alt="Brand" src="files/img/loader.gif">
</div>
</div>
<table class="table table-bordered table-hover" ng-show="nameSearchResults.length">
<tr>
<th>User ID</th>
<th>User Name</th>
</tr>
<tr ng-repeat="entry in nameSearchResults" ng-click="selectName(entry)">
<td>{{entry.user_id}}</td>
<td>{{entry.name}}</td>
</tr>
Currently, each time I need a different user id/name for a role, I have to copy the functions and change the names and the bar number...I really think there has to be a way to make this so I can call it multiple times and just pass in the model values I want. While the code above does not give me any errors, it does not provide me the input field and functionality to perform the lookup....please help.
I had to change my directive a little based on the issues pointed out, plus one I figured out after those changes were made. I needed changes to my directive and a change to my HTML, so I will post the final versions that appear to be working.
app.directive('nameUser', function() {
return {
restrict: 'E',
scope: { //CHANGED TO 'SCOPE'
idModel : '=',
nameModel : '='
},
templateUrl : 'app/views/nameUser.html', // ADDED 'Url'
link : function($scope,nameModel,idModel){ // ADDED THE TWO SCOPE NAMES
$scope.searchName = function(searchQuery, bar){
var bar = "#loadingBar" + bar;
if(searchQuery && searchQuery.length >= 3){
$(bar).slideDown();
$http.get("/read/userinfo/" + searchQuery).success(function(data){
$scope.nameSearchResults = data;
$(bar).slideUp();
});
}
}
$scope.selectName = function(pl){
$scope.nameModel = pl.name; //CHANGED BY ADDING $SCOPE AND ASSIGNING THE VALUE
$scope.idModel = pl.user_id; //CHANGED BY ADDING $SCOPE AND ASSIGNING THE VALUE
$scope.nameSearchResults = {};
}
}
};
My HTML changed (inModel to id-model and nameModel to name-model):
<name-user id-model="request.requesterID" name-model="request.requester"></name-user>
Thanks again for the help.

Dynamically create ng-table directive from another directive on same element

I'm trying to create wrapper directive for ng-table directive. My wrapper should instantiate ng-table directive on same element as the first directive is applied to and add some custom configuration to ng-table.
I am using following code to create ng-table directive.
angular.module('main')
.directive('mkTable', function($compile) {
return {
'link': function ($scope, element, attributes) {
element.removeAttr('mk-table'); // Must remove attribute because of recursion
element.attr('ng-table', 'tableParams');
$compile(element)($scope);
}
}
})
It does create ng-table (you can see it by pagination) but it doesn't display any data. If you check console output you can see that getData() function is called.
I presume that problem is in compiling child elements (tr, td) and bounding it to new ng-table scope, but I was not able to find the solution.
Demo: http://plnkr.co/1aEAdr2ugl39WG9Ay0vN
I think the problem is ng-repeat on tr element is being compiled couple of times, so I did a little naughty trick :) -insert "fake" to break Angular binding-
<tr fake-ng-repeat="user in $data">
<td data-title="'Name'">{fake{user.name}}</td>
<td data-title="'Age'">{fake{user.age}}</td>
</tr>
Then in the directive remove all "fake(s)" before recompiling:
element.html(element.html().replace(/fake-?/g, ''));
Demo.
Although it's working, I believe it's dirty trick.
After a lot of experimenting I figured it out. Solution is to use compile function insted of link.
angular.module('main')
.directive('mkTable', function($compile) {
return {
compile: function(element, attributes) {
element.removeAttr('mk-table');
element.attr('ng-table', 'tableParams');
var compileFn = $compile(element);
return function($scope, element, attributes) {
compileFn($scope);
}
}
}
})
Updated demo: http://plnkr.co/vL4kg0KVp4GYEDpOlIrm

Taking function from controller and placing in directive

I've noticed my angular controller is growing and ideally should be used for passing data.
I have a function that is currently contained within my controller that is called from my HTML to calculate how many months worth of data has been displayed (within a 12 month period) and if less than 12, return the remaining as empty/no payment:
JS:
$scope.getEmptyCells = function(len){
var emptyCells = [];
for(var i = 0; i < 12 - len; i++){
emptyCells.push(i);
}
return emptyCells;
}
HTML:
<table>
<tr ng-repeat="payments in MyPayments">
<th>{{payments.name}}</th>
<td ng-repeat="paymentAmount in payments.Details.slice(0, 12)">
{{ paymentAmount }}
</td>
<td ng-repeat="emptyCell in getEmptyCells(payments.Details.length)" class="empty">
No Payment
</td>
</tr>
</table>
myNewDirective:
app.directive('ngGetEmptyCells', function () {
return {
restrict: 'EA',
template: '<td ng-repeat="emptyCell in getEmptyCells(payments.Details.length)" class="empty">No Payment</td>',
controller: [
function (len) {
var emptyCells = [];
console.log("ngGetEmptyCells - STARTED");
console.log("len = " + len);
for (var i = 0; i < 12 - len; i++) {
emptyCells.push(i);
}
return emptyCells;
}
]
};
});
MY new HTML:
<table>
<tr ng-repeat="payments in MyPayments">
<th>{{payments.name}}</th>
<td ng-repeat="paymentAmount in payments.Details.slice(0, 12)">
{{ paymentAmount }}
</td>
<ng-get-empty-cells></ng-get-empty-cells>
</tr>
</table>
My fiddle: http://jsfiddle.net/oampz/8hQ3R/9/
Your controller (in your directive) is incorrect. You can set the method getEmptyCells on your scope of your directive if you do it like this instead.
controller: function($scope) {
$scope.getEmptyCells = function(len){
var emptyCells = [];
for(var i = 0; i < 12 - len; i++){
emptyCells.push(i);
}
return emptyCells;
};
}
Although since you do not declare an isolated scope in your directive (nothing wrong with that), your directive should be able to access the parent scope where you could have left your getEmptyCells method. Actually not relying on the parent scope helps keeping your directives modular.
If this fails to work, provide a plunker (or equivalent) example.
EDIT: You really should NOT prefix your own directives with ng as those are considered native Angular directives
EDIT: I moved your fiddle to plunker as Angular seems to work better there. I posted a working example:
http://plnkr.co/edit/e11zA8LKvoPTgTqW2HEE
I changed the code to use attributes instead of elements. There seems to be some problems for angular to correctly insert the td's into the row if you are using E instead of A.
EDIT: I changed the syntax <td get-empty-cells payments="payment"> to <td get-empty-cells="payment"> for easier usage. You can view the old plunker version (through its interface) for comparison and perhaps help understanding.
You can pass data into a directive by reference or value. You have to pass at least getEmptyCells by reference in order to be able to call it. Here is how you do it:
http://jsfiddle.net/8hQ3R/12/
Using directive:
<my-empty-cells get-empty-cells="getEmptyCells" payments="payments"></my-empty-cells>
Declaring isolated scope with getEmptyCells passed by reference and payments by value:
scope: {
getEmptyCells: '=',
payments: '#'
}
BUT:
You're going to have problems with this directive template because it has to have single root element and you're having multiple table rows. I would recommend iterating via 1-12 or even months array with ngRepeat and using separate scope function to extract either actual data or empty cell placeholder from model.

Disabling orderBy in AngularJS while editing the list

After applying orderBy in my list of input boxes if i edit any field it starts sorting immediately and i loose focus. I tried to dug in the angular code and found out they have applied something like $watch on the orderBy's attributes therefore whenever the value changes it sorts the list.Is there any way to disable orderBy while editing? I dont want to let the orderBy sort data while editing text. Any help will be appreciated
Here is my plunker
Note: I want to use orderBy & don't need any alternative like sorting the list from any controller itself. I just want the orderBy to sort the list once on page load and then remain quite.
I was using orderBy on a list that could be re-ordered using angular-sortable, and ran into a similar issue.
My solution was to perform the ordering manually, by calling the orderBy filter inside the controller when the page was initialised, and then calling it subsequently when necessary, for example in a callback function after re-ordering the list.
You could override the directive to change the moment of the update to the moment you wish the reordering. You could also just not use ng-model and rely on a custom directive.
This thread discuss overriding the input directive to change the model update to be triggered by tge blur event. Take a look at the fiddle.
Although you might override the directive, you shouldn't do this, and the best solution, as explained and exemplified by #Liviu T. in the comments below would be to create a custom directive that removes the event keyup binding and adds a blur one. Here is directive code, and here is Liviu's plunker:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
<input type="text" ng-model="variable" model-change-blur/>
Unfortunately, as Angular events are not namespaces, you will have to remove any previously added event.
A different approach may be to not loose focus to begin with. If your main problem is that your loosing focus, then instead of disabling orderBy, add this directive to your input:
app.directive("keepFocus", ['$timeout', function ($timeout) {
/*
Intended use:
<input keep-focus ng-model='someModel.value'></input>
*/
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $element, attrs, ngModel) {
ngModel.$parsers.unshift(function (value) {
$timeout(function () {
$element[0].focus();
});
return value;
});
}
};
}])
Then just:
<input keep-focus ng-model="item.name"/>
I know this does not directly answer you question, but it might help solve the underlying problem.
Following #CaioToOn works but it breaks in Angular 1.2.4 (possibly the whole 1.2.X branch). To get it to work you also need to declare the priority of the custom directive to be 1 (the input directive is 0).
The directive then becomes:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
See http://plnkr.co/edit/rOIH7W5oRrijv46f4d9R?p=preview and https://stackoverflow.com/a/19961284/1196057 for related priority fix.
There's no easy or straightforward way to accomplish what you want, but there are some options. You would have to work without ng-model, and instead use the blur event:
JS:
var app = angular.module('myapp', []);
app.controller('MainCtrl', function($scope) {
$scope.items = [
{ name: 'foo', id: 1, eligible: true },
{ name: 'bar', id: 2, eligible: false },
{ name: 'test', id: 3, eligible: true }
];
$scope.onBlur = function($event, item){
// Here you find the target node in your 'items' array and update it with new value
_($scope.items).findWhere({id: item.id}).name = $event.currentTarget.value;
};
});
app.directive('ngBlur', function($parse) {
return function ( scope, element, attr ) {
var fn = $parse(attr.ngBlur);
element.bind( 'blur', function ( event, arg ) {
scope.$apply( function(){
fn(scope, {
$event : event,
arg: arg
});
});
});
};
});
HTML:
<div ng-controller="MainCtrl">
<div ng-repeat="item in items | orderBy:'name'" >
<input value="{{item.name}}" ng-blur="onBlur($event, item)"/>
</div>
</div>
Plunker.
But be aware that this would break the two-way binding between the model and the view.
You can create custom filter and call that only when necessary. Example when you click on 'Grid header' for sorting or after dynamically adding/removing values to array, or simply click of a button(Refresh Grid)
You need to dependency Inject Angular filter and sort filter
angular
.module('MyModule')
.controller('MyController', ['filterFilter', '$filter', MyContFunc])
function ExpenseSubmitter(funcAngularFilter, funcAngularFilterOrderBy) {
oCont = this;
oCont.ArrayOfData = [{
name: 'RackBar',
age: 24
}, {
name: 'BamaO',
age: 48
}];
oCont.sortOnColumn = 'age';
oCont.orderBy = false;
var SearchObj = {
name: 'Bama'
};
oCont.RefreshGrid = function() {
oCont.ArrayOfData = funcAngularFilter(oCont.ArrayOfData, SearchObj);
oCont.ArrayOfData = funcAngularFilterOrderBy('orderBy')(oCont.ArrayOfData, oCont.sortOnColumn, oCont.orderBy);
}
}
and call in HTML something like:
<table>
<thead>
<tr>
<th ng-click="oCont.sortOnColumn = 'age'; oCont.RefreshGrid()">Age</th>
<th ng-click="oCont.sortOnColumn = 'name'; oCont.RefreshGrid()">Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="val in oCont.ArrayOfData">
<td>{{val.age}}</td>
<td>{{val.name}}</td>
</tr>
</tbody>
</table>
You can freeze the current ordering while you are editing. Say your html looks like this:
<tbody ng-repeat="item in items | orderBy:orderBy:reverse">
<tr ng-click="startEdit()">
<td>{{item.name}}</td>
</tr>
</tbody>
In your controller you write:
var savedOrderBy, savedReverse;
$scope.startEdit() = function() {
$scope.items = $filter('orderBy')($scope.items, $scope.orderby, $scope.reverse);
for (var i = 0; i < $scope.items.length; ++i) {
if (i < 9999) {
$scope.items[i]['pos'] = ("000" + i).slice(-4);
}
}
savedOrderBy = $scope.orderBy;
savedReverse = $scope.reverse;
$scope.orderBy = 'pos';
$scope.reverse = false;
};
Before the user starts editing, you first sort the current items in exactly the same order that they currently appear in the page. You do that by calling the orderBy $filter() with the current sorting parameters.
Then you go over your - now sorted - items, and add an arbitrary property (here "pos") and set it to the current position. I zero-pad it so that position 0002 collates before 0011. Maybe that is not necessary, no idea.
You normally want to remember the current ordering, here in the scope variables "savedOrder" and "savedReverse".
And finally you tell angular to sort by that new property "pos" and voilĂ  the table order is frozen, because that property simply does not change while editing.
When you are done editing, you have to do the opposite. You restore the old ordering from the scope variables "savedOrder" and "savedReverse":
$scope.endEdit = function() {
$scope.orderBy = savedOrderBy;
$scope.reverse = reverse;
};
If the order of the $scope.items array matters for you, you would also have to sort it again to its original ordering.
Try using a scope variable to change the order. In this case, when you are going to order, you call a function in your controller that changes the variable order value to the field you want to order by, and when you are editing, you reset it.
Example:
<li ng-repeat="item in filtered = (items | filter:search) | orderBy:order:rever" >
So here we have the variable "order" to tell the ng-repeat by which field the list must be ordered. A button calls a function in the controller that changes the "order" value.
<button type="button" id="orderName" click="changeOrder('item.name')" >Name order </button>
<button type="button" id="orderDate" click="changeOrder('item.date')" >Date order </button>`
And then, in the changeOrder function
$scope.order = param;
Where 'param' is the field you want to order by. If you don't do anything else, you are going to have the same problem you had, so then, after you have assigned the correct value to the order variable you go
$scope.order = "";
Which resets the ordering.
The result is that the ordering is just going to be effective when the button is pressed and then it wont order again unless you press the button again.
This can be changed so it orders again when, for example, you have finished editing an item, as you wanted.

Resources