Reputation: 148
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?
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...
Upvotes: 0
Views: 432
Reputation: 2905
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>
Upvotes: 2
Reputation: 504
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();
}
});
}
};
});
Upvotes: 0