Apply an Angular directive only after data is loaded - angularjs

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

Related

AngularJS directive runs before element is fully loaded

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.

Dynamically loading an external template in AngularJS

I'm trying to build an editable table component in AngularJS. The way I want it to work is that when the user clicks an Edit button on a particular row that row is replaced with an "edit template" containing input fields bound to the model. You can see my progress in this Plunker.
I'm using a custom directive to allow me to define a table with editable rows like so:
<table ng-controller="peopleController as peopleCtrl">
<tr editable-row edit-model="person" edit-tmpl="'person-editor.html'" ng-repeat="person in peopleCtrl.people">
<td>{{person.name}}</td>
<td>{{person.age}}</td>
<td>
<button>Edit</button>
</td>
</tr>
</table>
In the editable-row directive's Link function I am creating the "edit template" as a string of html and using $compile to bind the expressions to the directive scope. What I would like to do instead of hard coding the html within the Link function is have the template loaded from an external file referenced from the directives "edit-tmpl" attribute. Note: Setting the templateUrl for the directive won't work as I only want the template to be loaded and injected into the DOM when the user clicks the edit button.
My question is two fold:
1) How can I load the html from the template file referred to by the "edit-tmpl" attribute within the Link function of my directive?
2) As I am new to Angular I am wondering if my approach is in keeping with the AngularJS way of things? From an angular design perspective is it a good idea to have the edit template specified in the HTML via an attribute like this and then loaded within the directive's Link function or is there a better approach that I am missing?
app.directive('editableRow', function($compile){
return{
restrict:'A',
replace:true,
scope: {editModel: '='},
link: function(scope, element, attr){
element.find('button').on('click', function(){
var htmlText = '<div ng-include="{{attr.editTmpl}}"></div>';
OR
var htmlText='<div ng-include src="{{attr.editTmpl}}"></div>';
// I don't know which would work for you. but this is the way to add dynamic template to your directive by just passing appropriate path of template to attr.editTmpl attribute;
var editTmpl = angular.element($compile(htmlText)(scope));
element.replaceWith(editTmpl);
});
}
};
});
But I just wonder with your directive. As here you are using replaceWith method (which will replace your template to existing one in row)
but how would you get your original template or ROW back after editing is done on a row? I'd like to see it brother.

How to use a dynamically generated value in a template in AngularJS

I have a custom form application written in AngularJS and now I need to use the data from the form in a template. But nothing I've tried seems to work.
I am creating a custom directive like this...
.directive('dynamicModel', ['$compile', function ($compile) {
return {
'link': function(scope, element, attrs) {
scope.$watch(attrs.dynamicModel, function(dynamicModel) {
if (attrs.ngModel == dynamicModel || !dynamicModel) return;
element.attr('ng-model', dynamicModel);
if (dynamicModel == '') {
element.removeAttr('ng-model');
}
// Unbind all previous event handlers, this is
// necessary to remove previously linked models.
element.unbind();
$compile(element)(scope);
});
}
};
}])
This is attached to a form element like this..
<div class="row" ng-repeat="field in customForm.fields">
<label>{{field.displayname}}
<input class="form-control" type="{{field.type}}" name={{field.variable}} dynamic-model="field.databind" placeholder="{{field.variable}}" required="{{field.isRequired}}"></label></div>
This part works great, the field is now 2 way bound to the input form.
However when I later tried to use the same method to show the value in a report computed from the form, I get "field.databind" or at best the resolved databind field name such as "currentUser.name" rather than the value, e.g. "devlux"
I've tried
<div class="row" ng-repeat="field in customForm.fields">
<p>{{field.name}} = {{field.databind}}</p>
Also
<p dynamicModel="field.databind"></p>
</div>
Nothing works unless I put it into an input element, which isn't what I'm trying to do here.
The dynamic model code was pulled off someone elses answer to a question about creating dynamic form elements, and honestly I think it's just a step beyond my comprehension. But assuming that "field.databind" will always be a string literal containing the name of an inscope model, how on earth do I access it in a normal template?
{{field.databind}} will be evaluated against the current $scope and will result in whatever $scope.field.databind is, for example the string currentUser.name.
Angular has no way of knowing that currentUser.name isn't the string you want, but actually another expression that you want to evaluate.
To evaulate it again you will need to add a function to your $scope that uses the $parse service.
For example:
$scope.parseValue = function (value) {
return $parse(value)($scope);
};
In HTML:
<div class="row" ng-repeat="field in customForm.fields">
<p>{{field.displayname}} = {{parseValue(field.databind)}}</p>
</div>
The argument that gets passed to parseDynamicValue will for example be currentUser.name. Then it uses the $parse service to evaulate the expression against the current $scope, which will result in for example devlux.
Demo: http://plnkr.co/edit/iPsGvfqU0FSgQWGwi21W?p=preview

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.

AngularJS Access DOM inside $watch function

I'm making a directive that resizes a div based on changes in the controller. I need to calculate the amount of available space left in the browser window when changes happen to the model. How do you pass in the element from the link function into the $watch function?
In short, how do I manipulate the DOM based on changes to the model?
var module = angular.module('cmsApp')
module.directive("changeWidth", function($timeout) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
width = element.width();
$scope.$watch('currentFolder', function(value){
// manipulate dom here
});
}
}
});
<!-- need to calculate the size of this -->
<div change-width class="col-md-9 right-pannel"></div>
I don't think Angular is even executing your directive based on your template code. It should be
<div change-width class="col-md-9 right-pannel"></div>
I know this is a source of errors if you are new to Angular. From the docs:
Angular uses name-with-dashes for its custom attributes and camelCase
for the corresponding directives which implement them)

Resources