Creating a custom repeater with ng-repeat - angularjs

I want to create a custom repeater directive and pass the expression to the ng-repeat inside of the directive template.
The reason for this is to provide a cleaner interface in the html, as I am also including other directives "under-the-hood".
http://jsfiddle.net/DeanIconWeb/Cg9RC/1/
Here is my html template:
<tr custom-repeater="person in people">
<td>{{person.name}}</td>
<td>{{person.gender}}</td>
<td>{{person.age}}</td>
</tr>
Here is my directive:
app.directive("customRepeater", function(){
return {
priority : 2000,
restrict: 'A',
replace : true,
transclude : true,
scope : {
ngRepeatExp : "#customRepeater"
},
template : "<tr ng-repeat='{{ngRepeatExp}}' ng-class-even=\"'even'\" ng-transclude></tr>"
}
});
In trying to make this work I kept getting the "Template must have one root element" error.
I did eventually do the following, but it's not what I really want.
<tr ng-repeat="person in people" custom-repeater>
<td>{{person.name}}</td>
<td>{{person.gender}}</td>
<td>{{person.age}}</td>
</tr>
Directive
app.directive("customRepeater", function($compile){
return {
priority : 2000, //must be compiled before the ng-repeat (priority 1000)
restrict: 'A',
compile : function(tElem, tAttrs){
tElem.attr("ng-class-even", "'even'" );
}
}
});

You could use the $compile service to do something like this (add your other attributes/directives as you see fit before you consume the service):
http://jsfiddle.net/mQS4f/
app.directive("customRepeater", function ($compile) {
return {
//priority: 2000,
//restrict: 'A', // This is implicit
//replace: true,
//transclude: true,
//template: "<tr ng-repeat='{{ngRepeatExp}}' ng-class-even=\"'even'\" ng-transclude></tr>"
link: function (scope, element, attrs) {
attrs.$set('ng-repeat', attrs.customRepeater);
element.removeAttr('custom-repeater');
$compile(element)(scope);
}
}
});

Related

Having multiple suffix for same directive (angularjs)

Let say we have the following directive:
app.directive('testList', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//do stuff
}
}
});
I would like to call this directive from multiple element in html
<button test-list-add></button>
<table test-list="listObject">
<tr ng-repeat="list in testList">
<td test-list-click>list.item</td>
</tr>
</table>
is it possible to get into the link function for each attribute starting with test-list (test-list-add, test-list, test-list-click)?
Thank you
You want to pass attributes to your directive and doing some action, depending on the attribute:
app.directive('testList', function () {
return {
restrict: 'A',
scope: {
'add': '#', // Text
'click': '&' // function
},
link: function (scope, element, attrs) {
// scope.add - your Text you passed
// scope.click(); - calls your function you passed
}
}
});
<button test-list add="test"></button>
<table test-list="listObject">
<tr ng-repeat="list in testList">
<td test-list click="myFunc()">list.item</td>
</tr>
</table>
You can't repeat a directive (what you obvis trying to do).
If you want to make multiple suffix for a directive, you have to declare them one by one:
app.directive('testListAdd', function () {...});
app.directive('testListclick', function () {...});
<test-list-add></test-list-add>
<test-listclick></test-listclick>

Angular directive for table as parent and theader and tbody as child

I´m trying to do a parent directive with 2 child directives.
The main reason to do this is to force that all the tables in the application use the same "template".
My problem here is that i need to copy the Dom inside the directives.
So my main directive is the table directive, and the frist child is the thead and the second child is the tbody.
This is the markup
<div my-table >
<my-thead>
<tr>
<th ng-click="alert('Id')"> Id </th>
<th ng-click="alert('Name')"> Name </th>
</tr>
</my-thead>
<my-tbody>
<tr ng-repeat ="item in List">
<td>{{item.Id}}</td>
<td>{{item.Name}}</td>
</tr>
</my-tbody>
</div>
The directive my-table wil generate all the code necessary for the struct of a table while the dir my-thead and my-tbody will create the tags thead and tbody and give them to do parent div.
My problem remains on copying the inner markup of my-thead and my-tbody since they are tr, any suggestion?
Next i present to you the most recent code i get so far with a plunker
myTable
angular
.module('App')
.directive('myTable', ['$compile',
function($compile){
var tableSettings = {};
return {
restriction :'AE',
scope : {},
replace : false,
link : function(scope,element){
var divContainerFluid = angular.element('<div>');
var divRowTable = angular.element('<div>');
var divColTable = angular.element('<div>');
divContainerFluid.addClass('container-fluid');
divRowTable.addClass('Row');
divColTable.addClass('col-lg-12');
var divTable = angular.element('<div>');
var table = angular.element('<table>');
table.addClass('table');
table.addClass('table-striped');
//add from thead
table.append(tableSettings.tHead);
//add from tbody
table.append(tableSettings.tBody);
divTable.append(table);
divColTable.append(divTable);
divRowTable.append(divColTable);
divContainerFluid.append(divRowTable);
$compile(divContainerFluid)(scope);
element.append(divContainerFluid);
},
controller: function () {
this.receiveTHead = function (tHead) {
tableSettings.tHead = tHead;
}
this.receiveTBody = function (tBody) {
tableSettings.tBody = tBody;
}
}
}
}]);
myHead
angular
.module('App')
.directive('myThead', [
function() {
return {
restriction: "E",
scope: true,
require: "^myTable",
replace: true,
transclude: true,
template:'<thead class="form-class" ng-cloak>' +
'</thead>',
link: function(scope, element, attrs, myTableCtrl, transclude)
{
transclude(function(clone){
element.append(clone);
});
myTableCtrl
.receiveTHead(element);
}
};
}
]);
myBody
angular
.module('App')
.directive('myThead', [
function() {
return {
restriction: "E",
scope: true,
require: "^myTable",
replace: true,
transclude: true,
template:'<thead class="form-class" ng-cloak>' +
'</thead>',
link: function(scope, element, attrs, myTableCtrl, transclude)
{
transclude(function(clone){
element.append(clone);
});
myTableCtrl
.receiveTHead(element);
}
};
}
]);
Plunker
Thanks, for the help.
UPDATE
missing listCtrl
angular.module('App',[])
.controller('listCtrl',['$scope','$timeout',
function($scope,$timeout){
$timeout(function () {
$scope.List = [ {Id : '1', Name : 'Name1'}, {Id : '2', Name :'Name2'}];
}, 1500);
}
]);
UPDATE
I recreated the problem from a different perspective. But the body doesn't get populated with the info in the scope.List, any ideas?
<div my-table>
<table>
<thead>
<tr>
<th ng-click="resort('Id')"> Id </th>
<th ng-click="resort('Name')"> Name </th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in List">
<td>{{user.Id}}</td>
<td>{{user.Name}}</td>
</tr>
</tbody>
</table>
</div>
myTable
angular
.module('App')
.directive('myTable', [
'$compile',
function($compile) {
return {
restriction: 'AE',
//scope: {
// plEmptytext: '#'
//},
transclude: true,
template:
'<div class="container-fluid">' +
'<div class="row">' +
'<div class="col-lg-12">' +
'<div ng-transclude></div>' +
'</div>' +
'</div>' +
'</div>',
link: function(scope, element) {
var table = element.find('table');
table.addClass('table');
table.addClass('table-striped');
var tHead = element.find('thead');
var tBody = element.find('tbody');
tBody.attr('ng-controller', 'listCtrl');
$compile(tHead)(scope);
$compile(tBody)(scope);
$compile(table)(scope);
}
};
}
]);
the listCtrl is the same
Plunker 2
SOLUTION:
For the ones who might come here my solution has to add to the directive tBody return{ controller: 'listCtrl' ...
A different approach that might still provide your desired "one template for all" goal would be writing an html template file, and referencing it whenever a table is used by declaring an ng-include. Similar to the old server-side includes, it gives a way to inject a block of html anywhere, anytime, without all the overhead of a directive.
We've actually stepped back from making so many directives and started using includes more when there isn't any work to be done in the directive.
Angular documentation on ngInclude
Sounds like you need a "Multi-slot Transclusion".
This is available in angular versions > 1.5
https://docs.angularjs.org/api/ng/directive/ngTransclude
For the ones who might come here my solution has to add to the directive tBody
return{ controller: 'listCtrl' ...
For me it was the solution I was looking for.

possible for directive to run each time when scope changes?

I want to write a directive to for a table TBODY to show some text when it is empty. I want to achieve this by writing a directive that detects if the table's TBODY has any child TR, if not then show some text.
I do not wish to use ng-if="model.entries.length == 0" because I might have a TR in there for creating new entry that won't belong to entries.
The directive I wrote currently only works one time because it only runs once. When entries changes the directive won't run again and therefore the empty text is still showing
baseModule.directive('emptyTbody', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (element.find('tr').length == 0) {
element.addClass('empty');
} else {
element.removeClass('empty');
}
}
}
});
Is it possible to write a directive that runs when scope changes like regular angular behavior? Or if this cannot be achieved through a directive, is there any other ways to achieve this?
Here is the Html
<tbody empty-tbody>
<tr ng-if="isCreating()">
<td>
<input ng-model="creatingItem.Name"/>
</td>
</tr>
<tr ng-repeat="item in model.entries" >
<td>
<input ng-model="item.Name"/>
</td>
</tr>
</tbody>
Create a isolate scope and put watch there
Like this
baseModule.directive('emptyTbody', function() {
return {
restrict: 'A',
scope: {
source: '='
},
link: function(scope, element, attrs) {
scope.$watch("source", function(nv) {
if (nv) {
if (nv.length == 0)
element.addClass('empty');
else
element.removeClass('empty');
} else
element.addClass('empty');
});
}
}
});
HTML
<tbody empty-tbody source="model.leps">
EDIT
If you wanna to use only from element .
You can use anonymous function to watch.
Try like this
baseModule.directive('emptyTbody', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(function() {
return element.find('tr').length;
}, function(nv) {
if(nv){
console.log("Table has data")
}
else
console.log("Table has no data");
});
}
}
});
JSFIDDLE

Require controller in directive when terminal is true

I can't load controller's parent in a directive. I have two directives: h-menu and h-menu-item. h-menu uses a controller, and h-menu-item requires that controller.
But h-menu directive has terminal = true, and with this I can't load controller. When I set terminal to false, I can load the controller.
JsFiddle: http://jsfiddle.net/gspVe/4/
html:
<div ng-app="test">
<h-menu>
<h-menu-item>
</h-menu-item>
</h-menu>
</div>
Here is the code js:
angular.module("test", [])
.controller("hMenu", function () {
this.msg = "controller was loaded";
return this;
})
.directive("hMenu", function($compile) {
return {
restrict: "E",
// comment this and controller will be loaded
terminal: true,
controller: "hMenu",
require: "hMenu",
link: function (scope, element, attrs) {
var ul = $("<ul/>");
ul.append(element.children());
$compile(ul)(scope);
element.replaceWith(ul);
}
};
})
.directive("hMenuItem", function($compile) {
return {
restrict: "E",
terminal: true,
require: "?^hMenu",
link: function (scope, element, attrs, controller) {
var li = $("<li/>");
if (controller)
li.html(controller.msg);
else
li.html("contoller not loaded!");
$compile(li)(scope);
element.replaceWith(li);
}
};
})
Just trying to understand what you're trying to do. If you want to replace HTML elements, why not use some of the facilities that a directive provides already?
For instance, it looks like you're trying to replace the directive elements with <ul> and <li>. Rather than define this as you've indicated, why couldn't you do the following:
angular.module("test", [])
.directive("hMenu", function() {
return {
restrict: "E",
transclude : true,
replace : true,
template : "<ul ng-transclude></ul>"
};
})
.directive("hMenuItem", function() {
return {
restrict: "E",
replace : true,
transclude : true,
template : '<li ng-transclude></li>'
};
})
Then, you can use the power of Angular to indicate menu items declaratively, for example:
<div ng-app="test">
<h-menu ng-init="menuitems=['Menu #1', 'Menu #2', 'Menu #3']">
<h-menu-item ng-repeat="m in menuitems">
{{m}}
</h-menu-item>
</h-menu>
</div>
This would show:
Menu #1
Menu #2
Menu #3
I've updated a jsfiddle at http://jsfiddle.net/gspVe/9/

directive not working inside <tr> that is ng-repeat bound

I have a table where the rows are repeated via ng-repeat.
I am trying to create a template that generates columns <td> for each row <tr>
app.directive("customtd", function(){
return {
restrict: 'E',
template: "<td>{{position.Name}}</td><td>{{position.Code}}</td>",
replace: true,
scope: {
position: '='
}
}
});
<table>
<tr ng-repeat="p in positions">
<customtd position="p"></customtd>
</tr>
</table>
The issue is my custom td template is not rendered at all.
Here I intend to replace <customtd> with n number of <td>s - which will be decided based on number of properties on my data object, but at the moment I am just trying to get a simple directive working that will output two columns.
MYPLUNKER : shows an instance of this issue and the directive code.
As pointed out in comments the template of a directive should have single root element. So I would suggest you to move the tr element to the template of the directive, like this: http://plnkr.co/edit/YjLEDSGVipuKTqC2i4Ng?p=preview
As Pavlo wrote, you can move the tr element to the template for the directive. Another option is to use a td element and directive that replaces your td with the template that you want to use.
<table>
<tr ng-repeat="p in positions">
<td replace-me template="mytemplate.html" position="p"></td>
</tr>
</table>
Directive replaceMe
.directive("replaceMe", ["$compile", '$http', '$templateCache', function ($compile, $http, $templateCache) {
return {
restrict: 'A',
scope: {
position: "="
},
link: function (scope, element, attrs) {
function getTemplate(template) {
$http.get(template, {cache: $templateCache}).success(function (templateContent) {
element.replaceWith($compile(templateContent)(scope));
});
}
scope.$watch(attrs.template, function () {
if (attrs.template) {
getTemplate(attrs.template);
}
});
}
}
}]);
mytemplate.html
<td>{{position.Name}}</td>
<td>{{position.Code}}</td>
<td another-my-directive></td>
plunker

Resources