Dynamically inserting directives/components into angular table cells - angularjs

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

Related

Add an action button to DataTables column header in AngularJS

I am trying to add a button to the heading of a column in angular-datatables, which when clicked will run a function. I have tried doing something like this.
DTColumnBuilder.newColumn('name').withTitle(function() {
return '<span>Name</span><button ng-click="runme()">Click me</button>'
})
In the same controller, runme() function is defined as:
$scope.runme = function() {
console.log('clicked');
}
But this is not triggered, it only sort the column data, no matter where i clicked on entire header section.
When you are using this approach you'll need to $compile the content of the <thead> (and anything else injected by DataTables you would like AngularJS to be aware of).
A good place to invoke $compile is in the initComplete callback :
$scope.dtOptions = DTOptionsBuilder.newOptions()
.withOption('initComplete', function() {
$compile(angular.element('thead').contents())($scope)
})
demo -> http://plnkr.co/edit/D72WPqkE3g2UgJTg
Remember to inject $compile to your controller, see for example Working with $compile in angularjs. (Lousy google does not even bother to fix the errors in their docs, so https://docs.angularjs.org/api/ng/service/$compile does not work).
Note: You could also go with static <table> markup
<table>
<thead>
<tr>
<th><span>Name</span><button ng-click="runme()">Click me</button></th>
</tr>
</thead>
<tbody></tbody>
</table>
Then AngularJS will connect $scope.runme to the ng-click, and only if you need additional bindings in the dynamic content inserted by DataTables, a $compile is needed.

How do I access data from a parent directive view?

I'm trying to build a set of directives that handle our standard table layout for our site. For this I created two directives, one for the table and one for the columns. The goal here is to be able to dictate the columns of the table in the parent view.
Here's the parent view (index.html) that instantiates the directives. Note that col-name indicates what attribute I want to show for each row in the table.
<management-grid>
<col-data col-name="name"></col-data>
</management-grid>
Here's the relevant aspects of the managementGrid directive.
.directive('managementGrid', function() {
return {
...
template:
'<table>' +
'<tbody>' +
'<tr ng-repeat="row in collection" ng-transclude="data">' +
'<!--The specific columns go here using col-data-->' +
'</tr>' +
'</tbody>' +
'</table>',
transclude: {
'data': '?colData'
},
controller: ($scope) => {
$scope.collection = [{'name': 'Test Row 1'},{'name': 'Test Row 2'}];
}
};
});
And the child directive which is really only used to convert to a td tag so the browser doesn't discard it before the directive renders.
.directive('colData', function() {
return {
...
scope: {
colName: '#'
},
template: '<td>{{colName}}: {{row[colName]}}</td>'
};
});
In the final directive colData, the row object is not accessible. I know I can use a link function to access the parent controller, but that will only allow me to access collection and I need to access row, which is created using ng-repeat.
Here's Fiddle showing the issue.
edit:
Ideally I could simply transclude into the colData directive and not have to pass a property name in. Something like <td ng-transclude></td> and in the parent view <col-data>{{row.name}}</col-data>
I was able to solve this using $parent in the child directive. This accesses the parent scope. See the updated fiddle here.

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.

Preserving Scope in ng-repeat ( not wanting child scope )

I might be missing something conceptually but I understand that ng-repeat creates child scopes but for my scenario this is undesirable. Here is the scenario. I have a 3way bind to a firebase dataset. The object is an object with n sub objects. In my current code structure I use ng-repeat to iterate and render these objects with a custom directive. The issue is that these objects are meant to be "live" ( meaning that they are 3-way bound. The highest level object is bound with angularfire $bind ).
So the simple scenario in my case would be where the ng-repeat created scope was not isolated from the scope that it was created from.
I am looking for ideas on how to do this? Or suggestions on other approaches.
This won't be a complete answer, but I can help with the angularFire portion, and probably an angular guru can fill in the blanks for you (see //todo).
First of all, don't try to share scope. Simple pass the variables you want into the child scope. Since you'll want a 3-way binding, you can use & to call a method on the parent scope.
For example, to set up this pattern:
<div ng-repeat="(key,widget) in widgets">
<data-widget bound-widget="getBoundWidget(key)"/>
</div>
You could set up your directive like this:
.directive('dataWidget', function() {
return {
scope: {
boundWidget: '&boundWidget'
},
/* your directive here */
//todo presumably you want to use controller: ... here
}
});
Where &boundWidget invokes a method in the parent $scope like so:
.controller('ParentController', function($scope, $firebase) {
$scope.widgets = $firebase(/*...*/);
$scope.getBoundWidget = function(key) {
var ref = $scope.widgets.$child( key );
// todo, reference $scope.boundWidget in the directive??
ref.$bind(/*** ??? ***/);
};
});
Now you just need someone to fill in the //todo parts!
You still have access to the parent scope in the repeat. You just have to use $parent.
Controller
app.controller('MainCtrl', ['$scope', function ($scope) {
$scope.someParentScopeVariable = 'Blah'
$scope.data = [];
$scope.data.push({name:"fred"});
$scope.data.push({name:"frank"});
$scope.data.push({name:"flo"});
$scope.data.push({name:"francis"});
}]);
HTML
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="item in data | filter: search">
<td>
<input type="text" ng-model="$parent.someParentScopeVariable"/>
<input type="text" ng-model="item.name">
</td>
</tr>
</table>
</body>
Plunker

How to dynamically add in custom directive from other custom directive

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.

Resources