AngularJS directive runs before element is fully loaded - angularjs

I have a directive attached to a dynamically generated <table> element inside a template. The directive manipulates the DOM of that table inside a link function. The problem is that the directive runs before the table is rendered (by evaluating ng-repeat directives) - the table is empty then.
Question
How can I make sure that the directive is ran after the table has been fully rendered?
<table directive-name>
<tr ng-repeat="...">
<td ng-repeat="..."></td>
</tr>
</table>
module.directive("directiveName", function() {
return {
scope: "A",
link: function(scope, element, attributes) {
/* I need to be sure that the table is already fully
rendered when this code runs */
}
};
});

You can't, in a general sense, be ever "fully sure" by just having a directive on the <table> element.
But you can be sure in certain cases. In your case, if the inner content is ng-repeat-ed, then if the array of items over which ngRepeat works is ready, then the actual DOM elements will be ready at the end of the digest cycle. You can capture it after $timeout with 0 delay:
link: function(scope, element){
$timeout(function(){
console.log(element.find("tr").length); // will be > 0
})
}
But, in a general sense, you can't be certain to capture the contents. What if the ngRepeated array is not there yet? Or what if there is an ng-include instead?
<table directive-name ng-include="'templates/tr.html'">
</table>
Or, what if there was a custom directive that worked differently than ngRepeat does?
But if you have full control of the contents, one possible way to know is to include some helper directive as the innermost/last element, and have it contact its parent directiveName when it's linked:
<table directive-name>
<tr ng-repeat="...">
<td ng-repeat="...">
<directive-name-helper ng-if="$last">
</td>
</tr>
</table>
.directive("directiveNameHelper", function(){
return {
require: "?^directiveName",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
ctrl.notifyDone();
}
}
})

Try wrapping in a $timeout the code from your link function as it will execute after the DOM is rendered.
$timeout(function () {
//do your stuff here as the DOM has finished rendering already
});
Don't forget to inject $timeout in your directive:
.directive("directiveName", function($timeout) {
There are plenty of alternatives but I think this one is cleaner as the $timeout executes after the rendering engine has finished its job.

A clean way would be to use something like lodash's _.defer method.
You can call it with _.defer(your_func, your_func_arg1, your_func_arg2, ...) inside your link to execute the method, when the current call stack has cleared and everything is ready.
This way, you don't have to estimate a $timeout by yourself.

Related

how to append directive restrict='A' to existing element

There is a need to append custom directive wich is restricted to 'A' (attribute) to some of the instances of others directives (the second one, e.g.) after it was rendered using ng-repeat.
<some-directive ng-repeat="item in vm.items"></some-directive>
that 'A'-directive is derived from uib-popover, but I suppose solution for pure uib-popover will also work. Also, it would be great to safely remove than appended popovers later.
Any suggestions how to implement it?
You can do it in this way::
Create custom directive with priority more than ng-repeat priority....
ng-repeat has 1000 priority.
angular.module('x').directive('customDir', function() {
return {
priority: 1001, // as ng-repeat has priority level 1000
restrict: 'A',
compile: function () {
return function () {...}
}
}
})
Uses::
<some-directive ng-repeat="item in vm.items" custom-dir ></some-directive>

angular access ng-repeat array from child directive

I had an idea for an angular directive but I'm not sure if it's possible.
I often have to delete and item from an ng-repeat array, so the de facto solution is to have a function on the scope:
$scope.remove = function(item) {
var index = $scope.items.indexOf(item);
$scope.items.splice(index, 1);
}
I'm constantly writing this boiler plate code for every ng-repeat and it would be nice to be able to do something like this instead:
<li ng-repeat="item in items">
<button ng-click-remove="item"></button>
</li>
Basically I'm thinking the directive will simply wrap ng-click, but then I start thinking, is it even possible to access the items array from my directive without knowing its name or using $parent?
The way I do it is create a service handling the deletion/Creation of said task, and then just use that wrapper in the controller. I mean, the way you are suggesting, you still need to write the same code, but just do it in the directive. Also, yes it is possible to access it from the directive, you can pass in the controller to it.
function dir() {
return {
controller: 'ctrl as ctrl',
link: function(scope, elem, attrs, ctrl){
console.log(ctrl.items);
console.log(attrs.ngClickRemove); // gets the value of item.
}
}
}
Though I would caution using the ng syntax since it should be reserved for Angularjs.

Dynamically create ng-table directive from another directive on same element

I'm trying to create wrapper directive for ng-table directive. My wrapper should instantiate ng-table directive on same element as the first directive is applied to and add some custom configuration to ng-table.
I am using following code to create ng-table directive.
angular.module('main')
.directive('mkTable', function($compile) {
return {
'link': function ($scope, element, attributes) {
element.removeAttr('mk-table'); // Must remove attribute because of recursion
element.attr('ng-table', 'tableParams');
$compile(element)($scope);
}
}
})
It does create ng-table (you can see it by pagination) but it doesn't display any data. If you check console output you can see that getData() function is called.
I presume that problem is in compiling child elements (tr, td) and bounding it to new ng-table scope, but I was not able to find the solution.
Demo: http://plnkr.co/1aEAdr2ugl39WG9Ay0vN
I think the problem is ng-repeat on tr element is being compiled couple of times, so I did a little naughty trick :) -insert "fake" to break Angular binding-
<tr fake-ng-repeat="user in $data">
<td data-title="'Name'">{fake{user.name}}</td>
<td data-title="'Age'">{fake{user.age}}</td>
</tr>
Then in the directive remove all "fake(s)" before recompiling:
element.html(element.html().replace(/fake-?/g, ''));
Demo.
Although it's working, I believe it's dirty trick.
After a lot of experimenting I figured it out. Solution is to use compile function insted of link.
angular.module('main')
.directive('mkTable', function($compile) {
return {
compile: function(element, attributes) {
element.removeAttr('mk-table');
element.attr('ng-table', 'tableParams');
var compileFn = $compile(element);
return function($scope, element, attributes) {
compileFn($scope);
}
}
}
})
Updated demo: http://plnkr.co/vL4kg0KVp4GYEDpOlIrm

Apply an Angular directive only after data is loaded

I have a directive applied to a table, the outer div has a controller applied to it:
<div ng-controller="MyController">
<table ng-mydirective>
...
</table>
</div>
The controller loads some data and then uses ng-repeat to create some rows/columns in the table.
This is fine so far.
The directive needs to access columns possibly generated by the data, but the directive runs before the controller.
Is it possible to run a directive after the data is loaded/rendered? Or is the only way to achieve this by using $watch on the dom of the table?
<table ng-show="myDataSource.length>0" ng-mydirective>
...
</table>
or how about applying that within your directive's template itself, i.e hide the table until after the data source is populated. But in both cases the problem is that what if you display a table whose query returned 0 results? That means you directive won't show at all. Either way the best way to handle this would be inside the directive. I.e. hide the table unless your $http call executed successfully.
If your directive isn't using an isolate scope, it will automatically receive access to the data member in the controller scope. If you reference that data member in the template, Angular automatically sets up a $watch on it and you don't have to do anything else; otherwise you will need to add your own $watch. If you're using an isolate scope then it's a little more complicated but a similar pattern holds.
You have two ways for doing this
Easy one is using ng-if for requested data field. For example
<table ng-mydirective ng-if="nameOfTheDataSource !== undefined">
...
</table>
Also you can send an attribute to the directive and watch it until data arrives. This is more complicated but seemless to the template level:
Template
<table ng-mydirective ng-model="nameOfTheDataSource">
...
</table>
Directive
angular.module(....)
.directive('ng-mydirective', function() {
return{
scope: {
....
ngModel: '='
}
link: function(scope, element, attrs) {
var firstload = scope.$watch('ngModel', function(value) {
if (scope.ngModel !== undefined) {
// Do what is needed
}
// Deregister itself
firstload();
}
}
}
}

is there a way to get the DOM object given a $$hashKey?

I have a <table> where each row has a couple of input type="text". I want to validate if an input is empty and if so, then add a CSS class to this input field which will display an error. All I got in the $scope is the $$hashKey, I know this is an unique value to identify an element of a ng-repeat list.
How could I get the DOM object given this $$hashKey?. I was digging using the Developer Tools but I didn't find it.
Instead of trying to manipulate the DOM (ie find and element and add/remove a class) from your controller (or service), you should be doing it from a directive.
Write a directive that will do the validation for you:
.directive('validateField', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ngModel){
scope.$watch(attrs.ngModel, function(newVal, oldVal){
var isValid = false;
// do some validation checking on newVal here
ngModel.$setValidity('tableInput', isValid);
});
}
};
});
As noted in the docs here, the $setValidity function will automatically add a class to the element for you, based on whatever key you provide. In this case, we provided a key of 'tableInput' so it will add a class of ng-invalid-table-input when the model is invalid, and a class of ng-valid-table-input when the model is valid.
So in your CSS, all you now have to do is create a rule with some special styles:
input.ng-invalid-table-input{
/* special styles go here */
}
input.ng-valid-table-input{
/* special styles go here */
}
And then you would use this in your view as such:
<table>
<tr ng-repeat="things in listOfThings">
<td ng-repeat="thing in things">
<input type="text" ng-model="someValue" validate-field />
</td>
</tr>
</table>
And then any input in your table will be dynamically (and automatically) validated and styled. Does that make sense? You'd have to modify the above example, but hopefully it points you in the right direction.

Resources