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
Related
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.
I have a table its data generated by ng-repeat
<table>
<tr ng-repeat="item in list" post-repeat-directive>
<td>{{item.col1}}</td>
<td>{{item.col2}}</td>
</tr>
</table>
I want to combine frist td if they have the same value in the adjacent tr after binding, but do nothing if they are different.
I've tried to use directive
app.directive('postRepeatDirective', function() {
return function(scope, element, attrs) {
if (scope.$last){
// combine if they have the same value
}
};
});
But in scope.$last, they are still {{item.col1}}, but not the value from "list". So all first td are combine to one.
What can I do?
Edit:
Plunkr
I've solved it myself using $timeout to execute the function delay a few time then ng-repeat is done
if (scope.$last) {
$timeout(function(){
combine_table(t);
}, 10);
}
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();
}
}
}
}
I'm new to AngularJS. I've learned that I can find elements in the DOM using queries like the following:
var e = angular.element(document.querySelector('#id'));
var e = angular.element(elem.querySelector('.classname'));
This is useful for finding elements by ID, or by CSS class name. However, I need to be able to find an element using a different approach. I have an element that looks like the following:
<div my-directive class='myContainer'>...</div>
I can't query on 'myContainer' because of how much its reused. For that reason, I would like to find any element with the attribute 'my-directive'. How do I search the DOM and find any element that makes use of 'my-directive'?
Rather than querying the DOM for elements (which isn't very angular see "Thinking in AngularJS" if I have a jQuery background?) you should perform your DOM manipulation within your directive. The element is available to you in your link function.
So in your myDirective
return {
link: function (scope, element, attr) {
element.html('Hello world');
}
}
If you must perform the query outside of the directive then it would be possible to use querySelectorAll in modern browers
angular.element(document.querySelectorAll("[my-directive]"));
however you would need to use jquery to support IE8 and backwards
angular.element($("[my-directive]"));
or write your own method as demonstrated here Get elements by attribute when querySelectorAll is not available without using libraries?
You haven't stated where you're looking for the element. If it's within the scope of a controller, it is possible, despite the chorus you'll hear about it not being the 'Angular Way'. The chorus is right, but sometimes, in the real world, it's unavoidable. (If you disagree, get in touch—I have a challenge for you.)
If you pass $element into a controller, like you would $scope, you can use its find() function. Note that, in the jQueryLite included in Angular, find() will only locate tags by name, not attribute. However, if you include the full-blown jQuery in your project, all the functionality of find() can be used, including finding by attribute.
So, for this HTML:
<div ng-controller='MyCtrl'>
<div>
<div name='foo' class='myElementClass'>this one</div>
</div>
</div>
This AngularJS code should work:
angular.module('MyClient').controller('MyCtrl', [
'$scope',
'$element',
'$log',
function ($scope, $element, $log) {
// Find the element by its class attribute, within your controller's scope
var myElements = $element.find('.myElementClass');
// myElements is now an array of jQuery DOM elements
if (myElements.length == 0) {
// Not found. Are you sure you've included the full jQuery?
} else {
// There should only be one, and it will be element 0
$log.debug(myElements[0].name); // "foo"
}
}
]);
Your use-case isn't clear. However, if you are certain that you need this to be based on the DOM, and not model-data, then this is a way for one directive to have a reference to all elements with another directive specified on them.
The way is that the child directive can require the parent directive. The parent directive can expose a method that allows direct directive to register their element with the parent directive. Through this, the parent directive can access the child element(s). So if you have a template like:
<div parent-directive>
<div child-directive></div>
<div child-directive></div>
</div>
Then the directives can be coded like:
app.directive('parentDirective', function($window) {
return {
controller: function($scope) {
var registeredElements = [];
this.registerElement = function(childElement) {
registeredElements.push(childElement);
}
}
};
});
app.directive('childDirective', function() {
return {
require: '^parentDirective',
template: '<span>Child directive</span>',
link: function link(scope, iElement, iAttrs, parentController) {
parentController.registerElement(iElement);
}
};
});
You can see this in action at http://plnkr.co/edit/7zUgNp2MV3wMyAUYxlkz?p=preview
I am using AngularJS v1.2.1.
The improved ng-bind-html directive allows me to trust unsafe Html into my view.
Example
HTML:
<div ng-repeat="example in examples" ng-bind-html="example.content()"></div>
JS:
function controller($scope, $sce)
{
function ex()
{
this.click = function ()
{
alert("clicked");
}
this.content() = function ()
{
//if
return $sce.trustAsHtml('<button ng-click="click()">some text</button>');
// no problem, but click is not called
//when
return $sce.parseAsHtml('<button ng-click="click()">some text</button>');
//throw an error
}
}
$scope.examples = [new ex(), new ex()];
}
My question is, how to bind HTML content that may contain Angular expressions or directives ??
If you need dynamic templates per element, as your question suggests, one solution would be to use $compile within a directive to parse the HTML within the context of the local scope. A simple version of this is shown in this Plunk.
An example directive:
app.directive('customContent', function($compile) {
return function(scope, el, attrs) {
el.replaceWith($compile(scope.example.content)(scope));
}
});
The corresponding HTML:
<div ng-repeat="example in examples">
<div custom-content></div>
</div>
Notice that, in the Plunk controller, I've pulled out the click function into the scope for simplicity, since in the template HTML you are calling click() in the context of the scope, not on the example object. There are a couple ways you could use a different click function for each example, if that's what you'd like to do. This egghead.io screencast has a good example of passing an expression into a directive explicitly; in your case, it could be a click function or the whole example object, depending on what you need.