Transclude in Angular directive putting elements inside a single 'span' - javascript

Here is my directive:
myapp.directive('envtable', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: '<table class="table" ng-transclude></table>'
};
});
This is how i use it in html (using bootstrap css)
<envtable>
<tr>
<td>OS</td>
<td>{{env.osName}}</td>
</tr>
<tr>
<td>OS Version</td>
<td>{{env.osVersion}}</td>
</tr>
</envtable>
However, the code generated looks like this in chrome:
<table class="table" ng-transclude=""><span class="ng-scope ng-binding">
OS
Windows 8
OS Version
6.2
</span></table>
As you can see, Angular just ignored all my tr/td tags and put the contents in a single span element. Why is this happening?
Btw, as an experiment, i tried using just a transcluded p tag in the envtable instead of the tr\td tags and in that case angular just adds a ng-scope class to the p tag. So why does it screw up these tr/td tags?

It turns out this works with restrict: 'A'
<table envtable>
<tr>
<td>OS</td>
<td>{{env.osName}}</td>
</tr>
<tr>
<td>OS Version</td>
<td>{{env.osVersion}}</td>
</tr>
</table>
Demo

Just provide another example in case your table template has other elements like thead
Plunker
app.directive('envtable', function() {
return {
replace: true,
transclude: true,
template: '<table class="NewTable"><thead><th>Col1</th><th>Col2</th><th>Col3</th></thead></table>',
link: function(scope, elem, attrs, controller, transcludeFn) {
var item = transcludeFn(scope, function(clone) {
return clone.children();
});
elem.append(item);
}
};
});
<table envtable>
<tbody>
<tr ng-repeat='r in rows'>
<td>{{r.col1}}</td>
<td>{{r.col2}}</td>
<td>{{r.col3}}</td>
</tr>
</tbody>
</table>

I think this may be a repeat but your solution is simple. Avoid using <table>!
If you remove the <table> tags, replace them with <div>'s with display: table styling it should work just fine.

Related

Div not showing on ng-click of element returned by custom directive

I have created a directive in which a table html is returned successfully. One of the columns is anchor link <td><a ng-click="showLogDiv()">Show Modified Data</a></td> on whose click i want to show a div containing further data belonging to that row but it doesnot show.
//logdetails.html - the templateUrl proprerty of my directive
<div>
<table class="table table-hover table-responsive">
<thead class="table-thead">
<tr style="background-color:#56a7d6">
<th>AccessLogId</th>
<th>EntityName</th>
<th>EntityId</th>
<th>RequestType</th>
<th>Modified Data</th>
<th>Creation Date</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in viewData.logdata">
<td>{{value.AccessLogId}}</td>
<td>{{value.EntityName}}</td>
<td>{{value.EntityId}}</td>
<td>{{value.RequestType}}</td>
<!--<td><a ng-click="showLogDetails({{value.ModifiedData| json}})">Show Modified Data</a></td>-->
<td><a ng-click="showLogDiv()">Show Modified Data</a></td>
<td>{{value.CreatedDate | date:'medium'}}</td>
</tr>
</tbody>
</table>
<!--<div ng-show="divShow">Hello</div> I want to show {{value.ModifiedData| json}} contents here but even hardcoded Hello value not shown -->
<div ng-show="divShow">Hello</div>
</div>
In controller i have
$scope.divShow = false;
$scope.showLogDiv = function () {
alert($scope.divShow);
$scope.divShow = true;
alert($scope.divShow);
};
My directive
.directive("myActivityLogs", function () {
return {
restrict: 'E',
replace: 'true',
//template: '<div></div>',
//template: '<b>{{viewData.logdata[1].ModifiedData}}</b>'
templateUrl: 'app/modules/appScreen/logdetails.html'
//scope: {
// logsData:'='
//},
//link: function (scope, element, attrs) {
//link: function () {
// alert(viewData.logdata);
//}
};
});
How to hide/show part of html returned by directive and also how can i bind data to that part?
I am new to angularjs and nothing makes sense right now so maybe i am doing things wrong way so please explain in detail it would be very helpful.
One way to do this is to use a directive controller. You can change your directive like this:
.directive("myDirective", function() {
return {
restrict: 'E',
replace: 'true',
templateUrl: './logdetails.html',
scope: {
viewData: '='
},
controller: function($scope) {
$scope.divShow = false;
this.showLogDiv = function() {
$scope.divShow = true;
};
},
controllerAs: 'ctrl'
};
})
And then change your template HTML as the following so that it uses the controller:
<div>
<table class="table table-hover table-responsive">
<thead class="table-thead">
<tr style="background-color:#56a7d6">
<th>AccessLogId</th>
<th>EntityName</th>
<th>EntityId</th>
<th>RequestType</th>
<th>Modified Data</th>
<th>Creation Date</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in viewData.logdata">
<td>{{value.AccessLogId}}</td>
<td>{{value.EntityName}}</td>
<td>{{value.EntityId}}</td>
<td>{{value.RequestType}}</td>
<td><a href ng-click="ctrl.showLogDiv()">Show Modified Data</a></td>
<td>{{value.CreatedDate | date:'medium'}}</td>
</tr>
</tbody>
</table>
<div ng-show="divShow">Hello</div>
</div>
Notice that I've used <a href ng-click="ctrl.showLogDiv()">. You can refer to this working plunker to know more.

angular (jquery timepicker) unable to get value of input

I want to make a custom directive with the jquery plugin timepicker. I'm not getting the input value in the console, it says undefined.
here's a plunkr
<table class="table table-bordered">
<thead>
<tr>
<th>Time From</th>
<th>Time To</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" ng-model="row1" size=6/ disabled>
</td>
<td>
<input type="text" ng-model="dup_row1 " size=6 timepicki/>
{{dup_row1}}
</td>
</tr>
</tbody>
</table>
var app = angular.module('myApp', []);
app.directive('timepicki', [
function() {
var link;
link = function(scope, element, attr, ngModel) {
element.timepicki();
};
return {
restrict: 'A',
link: link,
require: 'ngModel'
};
}
])
app.controller('ctrl', function($scope) {
$scope.row1 = "00:00"
$scope.submit=function(){
console.log($scope.dup_row1)
}
});
The code you've posted is not the same as the code in your plunker.
The AngularJS Developer Guide says;
Use controllers to:
Set up the initial state of the $scope object.
In your example above, you log the value of $scope.dup_row1 on submit, but your controller never sets that value, and as such it is undefined.
The following will print "hello" to the console;
app.controller('ctrl', function($scope) {
$scope.row1 = "00:00"
$scope.dup_row1 = "hello"
$scope.submit=function(){
console.log($scope.dup_row1)
}
});

ng-repeat and custom directive issue in thead

I am trying to move our thead generation to a directive, but when using this, the headers lose their styling and end up bunching up on the left. Can anyone offer some guidance as to what I'm doing wrong?
The headers appear like this:
Col1Col2Col3Col4Col5
Instead of this when I use the same html without a directive. Note that these are properly aligned above their data columns:
Col1 Col2 Col3 Col4 Col5
index.html
<table class="table table-hover">
<thead>
<table-headers></table-headers>
</thead>
...
...
</table>
directives.js
app.directive('tableHeaders', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
priority: 1001,
templateUrl: '/PATH/TO/PARTIAL/table-headers.html'
};
});
table-headers.html
<tr>
<th ng-cloak="true" ng-repeat="header in coreHeaders" ng-click="setOrderBy(header)"> {{header}} <i class="fa" ng-show="header == ordering.currentHeader" ng-class="ordering.reverse ? 'fa-caret-up' : 'fa-caret-down'"></i></th>
<th class="align-right" ng-cloak="true" ng-repeat="header in statHeaders" ng-click="setOrderBy(header)">{{header}} <i class="fa" ng-show="header == ordering.currentHeader" ng-class="ordering.reverse ? 'fa-caret-up' : 'fa-caret-down'"></i></th>
</tr>
The only element you can have as a <thead>'s child is <tr>. Even if you have an Angular's directive which replaces it's original markup with proper tr elements, the browser will be faster ;) and will mangle your html before directive compilation so the results may be quite unexpected.
Try implementing table-headers as an attribute (restrict: 'A', and remove the replace: true option) instead. This way it will be a valid DOM tree from the start.

AngularJS does not compile element that has ng-repeat

I created an attribute directive that replaces the content inside of the element where it is used with a loading icon, until the variable that is the value of the attribute is evaluated to something other than undefined. The code for the directive is:
.directive('ngLoading', function (Session, $compile) {
var loadingSpinner = '<div class="spinner">' +
'<div class="rect1"></div>' +
'<div class="rect2"></div>' +
'<div class="rect3"></div>' +
'<div class="rect4"></div>' +
'<div class="rect5"></div></div>';
return {
restrict: 'A',
link: function (scope, element, attrs) {
var originalContent = element.html();
element.html(loadingSpinner);
scope.$watch(attrs.ngLoading, function (val) {
if(val) {
element.html(originalContent);
$compile(element.contents())(scope);
} else {
element.html(loadingSpinner);
}
});
}
};
});
I use this directive in my view in the following way:
<div ng-loading="user">
{{user.name}}
</div>
The content of this div is replaced by a laoding icon until the scope variable user contain some data, at which point the original content of the div is put back inside of the div and the div is compiled by $compile.
This works fine in most cases but it doesn't work when the original content of the div has a ng-repeat directive somewhere. The following case does not work, for instance:
<div ng-loading="users">
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{user.name}}</td>
<td>{{user.age}}</td>
</tr>
</tbody>
</table>
</div>
The table gets rendered but no tr inside tbody is rendered, as if the scope variable users was null. When I debug the code, I can see that the value of the variable users is indeed an array of users, right before the $compile call. I've tried wrapping the code inside the $watch in my directive with an $apply call, but when I do that I get the error "$apply already in progress".
It's also worth noting that the controller whose scope encompasses the div has a property called users.
What I am doing wrong?
UPDATE
I changed my html to:
<div ng-loading="users">
{{users[0]}}
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{user.name}}</td>
<td>{{user.age}}</td>
</tr>
</tbody>
</table>
</div>
After the loading finishes, the content of the div is replaced by the toString of the first user in the array, with all the correct information, followed then by the empty table. This really seems to be a problem with ng-repeat...
Possibly approach it differently - in Angular that sort of DOM manipulation isn't usually preferred.
Given the two way data binding of Angular, would a conditional show/hide in the html not accomplish the desired result while letting Angular take care of the details?
eg.
<div ng-show="users">
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{user.name}}</td>
<td>{{user.age}}</td>
</tr>
</tbody>
</table>
</div>
<div class="spinner" ng-hide="users">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
Instead of getting the html content and attaching the element content, just append the spinner to the content and make css changes to show at the top, once the loading completed, just remove the spinner element as like the below.
app.directive('ngLoading', function ($compile) {
var loadingSpinner = '<div class="spinner">' +
'<div class="rect1"></div>' +
'<div class="rect2"></div>' +
'<div class="rect3"></div>' +
'<div class="rect4"></div>' +
'<div class="rect5"></div></div>';
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(attrs.ngLoading, function (val) {
if (val) {
element.append(loadingSpinner);
} else {
element.find('div.spinner').detach();
}
});
}
};
});

Updating the scope through directive. Best practices

I am struggling with the concept of scope while working with a directive. I read through the offical docs as well as half a dozen blogs and stuff, but i am not able to get this done.
I have 5 fields for text input. What i want to achive is, if one field is filled, the next sibling shall appear:
<table>
<tr ng-repeat="entry in activeField.entries">
<td entry-value contenteditable="">{{entry.value_0}}</td>
<td entry-value contenteditable="" ng-show="entry.value_0">{{entry.value_1}}</td>
<td entry-value contenteditable="" ng-show="entry.value_1">{{entry.value_2}}</td>
<td entry-value contenteditable="" ng-show="entry.value_2">{{entry.value_3}}</td>
<td entry-value contenteditable="" ng-show="entry.value_3">{{entry.value_4}}</td>
</tr>
</table>
This is my directive entryValue
terminal.directive('entryValue',function(){
return{
restrict: 'A',
controller: 'terminalController',
link: function(scope, element){
element.bind('input',function(){
// Here i want to update the scope
})
}
}
});
As the user makes an input, i want to push that value to my controllers scope, which contains the object activeField. I have tried to bind $watch to my directives linking function as well as scope.$apply() but neither works.
I'd appreciate any help
<tr ng-repeat="entry in activeField.entries">
<td entry-value contenteditable=""
ng-hide="!$first && !activeField.entries[$index-1].value">{{entry.value}}</td>
</tr>
app.directive('entryValue',function(){
return{
replace: true,
controller: 'terminalController',
template: '<div><input ng-model="entry.value"></div>',
link: function(scope, element){
}
}
});
$scope.activeField = {entries:
[ {value: ''}, {value: ''}, {value: ''}, {value: ''}, {value: ''} ] };
fiddle

Categories