AngularJS does not compile element that has ng-repeat - javascript

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();
}
});
}
};
});

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.

directive data sometimes does not appear after broadcast

I'm getting this annoying bug with Angular where I broadcast data to a directive but the directive's $on doesn't receive it.
Therefore, my table doesn't populate at all and looks terrible to users.
test_results.html (contains an instance of the directive):
<div>
<h1>Test Results</h1>
...
<results></results>
</div>
resultsCtrl.js controller:
$timeout(function () {
$rootScope.$broadcast('show-results', test_session.question_objects);
}, 100);
results.html directive template (most fields stripped out):
<div class="results">
<table class="table table-bordered table-striped">
<thead>
<tr>
<td ng-if="average_times && !$root.is_mobile">
<span ng-click="sortType = 'question.average_timing'; sortReverse = !sortReverse">
Avg. Time
</span>
</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="q in questions | orderBy:sortType:sortReverse | filter:searchQuestions track by $index">
<td ng-if="q.average_timing && !$root.is_mobile" ng-click="$root.openDirections('question', q)">{{ q.average_timing }}</td>
</tr>
</tbody>
</table>
</div>
results.js directive:
scope.$on('show-results', function(event, test_session) {
setTestSessionData(test_session);
});
...
function setTestSessionData (test_session) {
scope.questions = test_session;
}
I cannot figure out when exactly this happens. At first I thought it's when I load the site for the first time, but I've tried that since and the data is rendered.
Whenever you do a $rootScope.$broadcast all the child scopes who have registered to those events via $scope.$on will catch the event. Same goes with directive scope also. If you update the model in the event listener , it will be updated in the view.
Working plunker : https://plnkr.co/edit/4iokGsfPH5IqNdecjaGd?p=preview

How to make dynamic content-editable table using angular JS?

Caveat: I've just started with client side scripting and Angular JS is the first thing I'm learning and now I feel I should've started with javascript.
PS: I don't wanna use any third party libraries. I wanna learn to code.
Anyway,I have dynamic table which I want to make editable using content-editable=true attribute of HTML.
Problem: How to I get the edited data? whenever I click on submit and pass the this object to the check() function. I doesn't contain edited values. is there a possible way to pass only edited value if it's dirty. It has pagination so If g to the next page the edited values are gone. I know I've give unique Id to every td element with $Index concatenated to it. But I don't know how should I proceed.
Any help or guidance will be appreciated. Controllers and others are defined in my route.
<div>
<form ng-submit="check(this)">
<table class="table table-striped table-hover">
<tbody>
<tr ng-repeat="data in currentItems">
<td contenteditable="true >{{data.EmpNo}}</td>
<td contenteditable="true">{{data.isActive}}</td>
<td contenteditable="true">{{data.balance}}</td>
<td contenteditable="true">{{data.age}}</td>
<td contenteditable="true">{{data.eyeColor}}</td>
<td contenteditable="true">{{data.fname}}</td>
</tr>
</tbody>
<tfoot>
<td>
<div class="pagination pull-right">
<li ng-class="{'disabled': previousPage}">
<a ng-click="previousPage()" >Previous</a>
</li>
<li ng-repeat="page in pageLengthArray track by $index">
<a ng-click="pagination($index)">{{$index+1}} </a>
</li>
<li disabled="disabled">
<a ng-click="nextPage()" ng-class="{'disabled':nextPage}>Next </a>
</li>
</div>
</td>
</tfoot>
</table>
<input type="submit" value="Submit">
</form>
$scope.currentPage=0;
$scope.pageSize=10;
$scope.currentItems;
$scope.tableData;
$http.get('../json/generated.json').then(function(response){
$scope.tableData=response.data;
$scope.pageLength=Math.ceil($scope.tableData.length/$scope.pageSize);
$scope.currentItems=$scope.tableData.slice($scope.currentPage,$scope.pageSize);
$scope.pageLengthArray= new Array($scope.pageLength);
});
$scope.pagination=function(currentPage){ $scope.currentItems=$scope.tableData.slice($scope.pageSize*currentPage,$scope.pageSize*currentPage+$scope.pageSize);
$scope.currentPage=currentPage;
}
$scope.nextPage=function nextPage(argument) {
$scope.currentPage++; $scope.currentItems=$scope.tableData.slice(($scope.pageSize*$scope.currentPage),($scope.pageSize*($scope.currentPage)+$scope.pageSize));
}
$scope.previousPage=function previousPage(argument) {
$scope.currentPage--;
$scope.currentItems=$scope.tableData.slice(($scope.pageSize*$scope.currentPage),($scope.pageSize*($scope.currentPage)+$scope.pageSize));
}
In the usual case, you can not get a change model for contenteditabe because to change the model used ngModel.
But we can create a directive that we have updated the value of the model.
Live example on jsfiddle.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope, $timeout) {
$scope.data = {
EmpNo: "123"
};
})
.directive('contenteditable', function($timeout) {
return {
restrict: "A",
priority: 1000,
scope: {
ngModel: "="
},
link: function(scope, element) {
element.html(scope.ngModel);
element.on('focus blur keyup paste input', function() {
scope.ngModel = element.text();
scope.$apply();
return element;
});
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<table>
<tr>
<td ng-model="data.EmpNo" contenteditable="true"></td>
</tr>
</table>
<pre>{{data|json}}</pre>
</div>
</div>
I would store any object that gets modified in a seperate array using the ng-keyup directive. When the form is submitted, you will have an array of only elements which have been modified. You may have some UX issues if your pagination is done by server as when you change page and come back, it will show your old data, but hopefully this helps.
$scope.check = function () {
// check modifiedItems
console.log(modifiedItems);
};
// store modified objects in a seperate array
var modifiedItems = [];
$scope.modifyItem = function (data) {
// check if data has already been modified and splice it first
for(var i = 0, j = modifiedItems.length; i < j; i++) {
var currentItem = modifiedItems[i];
if (currentItem.id === data.id) {
modifiedItems.splice(i, 1);
break;
}
}
// add to modified
modifiedItems.push(data);
console.log('modifiedItems: ', modifiedItems);
};
HTML
<form ng-submit="check()">
<table class="table table-striped table-hover">
<tbody>
<tr ng-repeat="data in currentItems">
<td ng-repeat="(key, value) in data" contenteditable="true"
ng-keyup="modifyItem(data)">
{{data[key]}}
</td>
</tr>
</tbody>
<tfoot>
</table>
<input type="submit" value="Submit">
</form>

How to dynamically change the content of a table column

Given the following HTML fragment, how can I create the content of the td depending on the column.
<div ng-app="" ng-controller="controller" >
<table>
<tr>
<th ng-repeat="column in columns">
{{ column.header }}
</th>
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
<!-- TODO -->
</td>
</tr>
</table>
</div>
Each column can show a different kinds of data. For example, one might just show a string, another might contain a text input field that is bound to a property of the row.
I would like to call a function on the column (column.createCell(row)) that creates that necessary HTML and then put the result in place of <!-- TODO -->.
In WPF, I would just put a ContentPresenter with a DataTemplateSelector, but I don't know what the equivalent is in Angular. I read about something called "ng-bind-html", is that the way to go?
It's not given what kind of custom element you want to build for each column, but for DOM manipulation in AngularJS best practise is to keep it in a directive. Something like this:
in your html:
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="row in rows">
<td ng-repeat="column in row">
<custom-column="column"></custom-column>
</td>
</tr>
</table>
</body>
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
// whatever you wanted to define here...
$scope.rows = ...
$scope.columns = ...
});
app.directive('customColumn', function() {
return {
scope: {
obj: '=customColumn',
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('obj', function(obj) {
if (!obj) return;
// build custom element
var html = '<div>'+scope.obj.name+'</div>';
element.html(html);
// delete watch if you only need to draw once
watcher();
});
}
}
});

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

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.

Categories