I'm a newbie in Angular.js and I stuck with one problem.
I want to integrate this plugin (https://github.com/pratyushmittal/angular-dragtable) to be able to drag columns in my table.
The whole table is a directive. Each <th> also renders by a directive.
<table>
<thead>
<tr>
<th ng-repeat="col in table.columns" my-column></th>
</tr>
</thead>
</table>
According to plugin documentation I need to set draggable directive to table. If I set it manually it doesn't grab my columns properly, because this columns is not rendered at that moment, and this doen't work. In my-column directive I'm waiting for last < th >
.directive('myColumn', ['$timeout', function($timeout) {
return {
restrict: 'A',
templateUrl: 'templates/column.html',
link: function(scope, element, attrs) {
if (scope.$last)
$timeout(function() {
//scope.$emit('lgColumnsRendered');
angular.element(element).closest('table').attr('draggable', 'draggable');
});
}
}
}])
And when last th is rendered I going up to my table and set this directive. For sure it is stupid and doesn't work. I also read about $compile but I need add attribute-directive to already existing table in my DOM.
Maybe I go wrong way and don't understand concept of doing this, but you catch the idea? How can I do this?
The problem is that angular-dragtable doesn't expect that table columns will be dynamic.
And I think it is logical assumption - in most cases table rows will be dynamic (which is OK for the dragtable), but columns are usually static.
The solution to this is to add a special event to the dragtable to ask it for re-initialization when your columns are created, here is the modification I made to dragtable (see the link to the full source below):
project.directive('draggable', function($window, $document) {
function make_draggable(scope, elem) {
scope.table = elem[0];
scope.order = [];
scope.dragRadius2 = 100;
var headers = [];
init();
// this is the event we can use to re-initialize dragtable
scope.$on('dragtable.reinit', function() {
init();
});
function init() {
headers = scope.table.tHead.rows[0].cells;
for (var i = 0; i < headers.length; i++) {
scope.order.push(i);
headers[i].onmousedown = dragStart;
}
}
function dragStart($event) {
Now in your code you can do this:
.directive('myColumn', ['$timeout', '$rootScope', function($timeout, $rootScope) {
return {
restrict: 'A',
templateUrl: 'templates/column.html',
link: function(scope, element, attrs) {
if (scope.$last)
$rootScope.$broadcast('dragtable.reinit');
}
}
Here is a full code of the example I tested the issue on.
Related
I am trying to populate a table based on an array of objects. This array doesn't contain objects of the same type and for each row I'd like a completetly diferent style and, onclick function, basically a completely different behaviour.
For instance,
var data=[
{
type:'dir-b',
data: { ... }
},
{
type:'dir-b',
data: { ... }
},
{
type:'dir-c',
data: { ... }
}
]
For object type dirB I want a template and controller and for dirC a completely different function and template.
The solution I found was to create 3 directives. One of wich will run to determine one of the other two directives to add based on data.
.directive("dirA", function($compile){
return{
restrict:'A',
priority:1000,
terminal:true,
link: function(scope, element, attribute){
element.removeAttr("dir-a");//prevent endless loop
element.attr(attribute.type,"");
$compile(element)(scope);
}
}
})
.directive("dirB", function($compile){
return{
restrict:'A',
replace:true,
link: function(scope, element, attribute){
console.log("dirA");
}
}
})
.directive("dirC", function($compile){
return{
restrict:'A',
replace:true,
link: function(scope, element, attribute){
console.log("dirC");
}
}
});
Using <tr dir-a type='{{d.type}}' ng-repeat='d in data'/> is not having the desired effect. Either I give dirA a priority of 0 and it can parse the attribute but it's repeated more times than the array size, or I give it a priority of 1000 and it can't parse the b.type and use it as a literal.
Does anyone have a solution for this?
You could potentially use an ngSwitch here.
Plnkr
HTML
<div ng-repeat="(key, d) in data track by $index">
<div class="tbody" ng-switch on="d.type">
<div class="row" ng-switch-when="dir-b" dir-b>{{d}}</div>
<div class="row" ng-switch-when="dir-c" dir-c>{{d}}</div>
</div>
</div>
Then you just define dirB and dirC directives.
This doesn't display as an html table though, you can hopefully work from this though?
Not sure this was the best solution but it was the solution I found.
<table>
<tbody ng-repeat='d in data'>
<tr ng-if='d.type=="dir-b"' dir-b></tr>
<tr ng-if='d.type=="dir-c"' dir-c></tr>
</tbody>
</table>
This way due to ng-if only the correct row will ever be displayed but the problem is that tbody will be repeated as many row as there are in data. But until there is a beter solution this is how I did it.
I have an AngularJS project (I don't use JQuery) where I need to display a table with users and load more as the user scrolls near the end of the page. I'm trying to implement this without relying on external libraries, since this is part of my requirements.
I've checked several examples like this, this and this.
But so far, none of the examples I've checked have helped me implement the infinite scrolling as I expected. They use different ways to calculate when to trigger the call, and some of this values return undefined to me (i.e. people say clientHeight and scrollHeight have different values, but to me it's always the same one, the total height including scrolling space).
I created a directive like the following:
usersModule.directive('whenScrollEnds', function($window) {
return {
restrict: "A",
link: function(scope, elm, attr) {
angular.element($window).bind('scroll', function() {
var hiddenContentHeight = elm[0].scrollHeight - angular.element($window)[0].innerHeight;
if ((hiddenContentHeight - angular.element($window)[0].scrollY) <= (10 )) {
angular.element($window)[0].requestAnimationFrame(function(){
scope.$apply(attr.whenScrollEnds);
})
}
});
}
};
});
This kind of works but there's the following problems/doubts which I hope someone can explain to me:
It triggers too fast. I want the loading to trigger when I'm near the bottom of the scrollable space, like near 90% or so.
The scrollHeight is only accesible through elm[0], angular.element($window)[0] has no scrollHeight property so it returns undefined, and elm[0] has no scrollY value.
The scrollY value I get is the distance the scrollbar has moved from the top, minus the scrollbar length, but I feel like that value is wrong.
Is binding the scroll event through angular.element($window).bind the right decision?
How can I implement a proper infinite scrolling table? Am I using the correct variables? Please provide an answer that uses Javascript and AngularJS only, JQuery or libraries solutions won't help me.
After checking several examples and trying to avoid those that used JQuery as part of the solution I found a way to make this work.
First my directive that would handle when the scroll ends:
usersModule.directive('whenScrollEnds', function($window) {
return {
restrict: "A",
link: function(scope, elm, attr) {
var raw = elm[0];
raw.addEventListener('scroll', function() {
if ((raw.scrollTop + raw.clientHeight) >= (raw.scrollHeight )) {
scope.$apply(attr.whenScrollEnds);
}
});
}
};
});
My view has the table inside a div like this:
<div id="usersScrollable" when-scroll-ends="uc.loadMoreUsers()">
<table id="tableUsers" class="table" ng-show="uc.users.length" >
<thead>
<tr>
<th>Email</th>
<th>Nombre Completo</th>
<th>Estado</th>
<th>Acción</th>
</tr>
</thead>
<tbody >
<tr ng-repeat="u in uc.users">
<td>{{u.email}}</td>
<td>{{u.fullName}}</td>
<td>{{uc.getStateString(u.active)}}</td>
<td><button type="button" class="btn btn-primary" ng-click="uc.edit(u)">Editar</button></td>
</tr>
</tbody>
</table>
</div>
Make sure the div that contains the table is the one listening to when the scrolling ends. This div has to have a set height, if it doesn't then the clientHeight property will be the same as scrollHeight (not sure what would happen if there's no height defined explicitly, like instead of setting the height you set the top or bottom properties).
In my controller, loadMoreUsers() is in charge of incrementing the page (in case there's more users, each call I get has the total of users so I can know before making another request how many users I have left), also it calls the function that makes the request to the web service.
The problem with the solution provided by Uriel Arvizu is that if the raw element is empty because it is waiting for a Http Request at the page load, all the following raw.scrollTop, raw.clientHeight) and raw.scrollHeight will have wrong dimensions and the scroll is no working anymore.
I would suggest this other solution that basically adds the scroll event to the $window without cacheing it, so it is always sized correctly when the Http response is received.
(function () {
'use strict';
angular.module('ra.infinite-scroll', []).directive('infiniteScroll', function ($window) {
return {
restrict: 'A',
scope: {
infiniteScrollCallbackFn: '&'
},
link: function (scope, elem, attrs) {
var percentage = (attrs.infiniteScrollPercentage !== undefined ? (attrs.infiniteScrollPercentage / 100) : '.9');
var $element = elem[0];
angular.element($window).on('scroll', function () {
if ((this.scrollY + this.innerHeight - $element.offsetTop) >= ($element.scrollHeight * percentage)) {
scope.$apply(scope.infiniteScrollCallbackFn);
}
});
}
};
});
})();
Also, with this module, you can pass 2 parameters in the HTML: a callback function and a percentage of the specific element (where the module is applied) that when it is reached the callback function is called, i.e. to repeat the Http request (default is 90% of that element).
<div infinite-scroll infinite-scroll-callback-fn="callBackFunction()" infinite-scroll-percentage="80">
// Here you may include the infinite repeating template
</div>
Using example code of Ferie, yet I had to use mousewheel event instead of scroll and had to use elem.scrollTop() instead of elem.scrollY
app.directive('infiniteScroll', function ($window) {
return {
restrict: 'A',
scope: {
infiniteScrollCallbackFn: '&'
},
link: function (scope, elem, attrs) {
var percentage = (attrs.infiniteScrollPercentage !== undefined ? (attrs.infiniteScrollPercentage / 100) : '.9');
var $element = elem[0];
angular.element($window).on('mousewheel', function () {
if ((elem.scrollTop() + this.innerHeight - $element.offsetTop) >= ($element.scrollHeight * percentage)) {
scope.$apply(scope.infiniteScrollCallbackFn);
}
});
}
};
});
when used in Angular table with a ng-repeat on the <tr>, I had to add the directive into the parent tbody of this tr in order to capture the right element containing the scroll state.
<tbody infinite-scroll infinite-scroll-callback-fn="callBackFunction()" infinite-scroll-percentage="80">
I currently have an AngularJS application embedded in an iframe which needs to be resized to avoid scrollbars. I've got a function in the app that calculates the height of the container and then resizes the iframe.
Currently I am using a directive (resizeAppPart) which will call the resize function on the last item in the scope.
Directive:
app.directive('resizeAppPart', function () {
return function (scope, element, attrs) {
if (scope.$last) {
Communica.Part.adjustSize();
}
}
});
Layout:
<tr ng-repeat="task in filteredTasks = (tasks | filter:filters)" resize-app-part>
<td>{{task.Task_x0020_Title}}</td>
<td><span ng-repeat="user in task.assignees" ng-show="user.Title != ''">
...
</tr>
This works on the initial load but if I filter the list using any of the search boxes, the directive doesn't run so you either end up with a scrollbar or a few thousand pixels of whitespace - neither are ideal.
Is there a way to call the directive, or even the function directly, after the table is filtered?
You need to put a $watch , Use this:
app.directive('resizeAppPart', function ($timeout) {
return function (scope, element, attrs) {
scope.$watch('$last',function(){
Communica.Part.adjustSize();
}
scope.$watch(attrs.filterWatcher,function(){
$timeout(function(){
Communica.Part.adjustSize();
},100)
})
}
});
And slight change in html this way
<tr ng-repeat="task in tasks | filter:filters" resize-app-part filter-watcher={{filters}}>
I have a directive whose data is being received via an api call. The directive itself works fine, the problem arises (I believe) because the directive is loaded before the api call finishes. This results in the whole shebang just not working. Instead of my expected output, I just get {{user}}.
My directive looks like this:
app.directive('myDirective', function() {
return {
restrict: 'A',
require: '^ngModel',
scope: {
ngModel: '=',
},
template: '<tbody style="background-color: red;" ng-bind-html="renderHtml(listing_html)"></tbody>',
controller: ['$scope', '$http', '$sce',
function($scope, $http, $sce) {
$scope.listing_html += "<td>{{user.name}}</td>"
$scope.renderHtml = function(html_code) {
return $sce.trustAsHtml(html_code);
};
}
],
link: function(scope, iElement, iAttrs, ctrl) {
scope.$watch('ngModel', function(newVal) {
// This *is* firing after the data arrives, but even then the
// {{user}} object is populated. And the `user in ngModel` doesn't
// run correctly either.
console.log(scope.ngModel);
scope.listing_html = "<tr ng-repeat='user in ngModel'><td>{{user}}</td></tr>"
})
}
};
});
And my html is simply
<table my-directive my-options='{"Name": "name", "Email": "email"}' ng-model='userData'></table>
I've created a plunker with a ton of comments to hopefully help explain the issue.
This question is very similar to this one, with the key distinction of that solution not working. Adding ng-cloak to mine just makes it not display.
It may also be worth noting that I've been using this as reference on the way to construct a directive.
I think you're making this a bit more complicated that it needs to be. If you're going to try to insert dynamic HTML with Angular expressions in them, you need to use the $compile service to compile them first (this hooks up the directives, etc, in that dynamic HTML to Angular). With that said, I don't think you need to do that for what you're trying to accomplish.
Take a look at this updated plunk: http://plnkr.co/edit/RWcwIhlv3dMbjln4dOyb?p=preview
You can use the template in the directive to produce the dynamic changes you need. In my example, I've used ng-repeat to repeat over the users provided to the directive, and also to the options provided to the directive. ng-repeat does the watching, so as soon as the data provided to the directive via ng-model is updated, the ng-repeats reflect those changes.
<tbody style="background-color: red;">
<tr><th ng-repeat="option in myOptions">{{option.name}}</th></tr>
<tr ng-repeat="user in ngModel">
<td ng-repeat="option in myOptions">{{user[option.value]}}</td>
</tr>
</tbody>
The options I defined in the main controller like this.
$scope.tableOptions = [
{"name": "Name", "value": "name"},
{"name": "Email", "value": "email"}
];
You could add other properties to this that are used by the directive, such as display order, etc. You could even remove an item from the options dynamically and that data would then be removed from the output table.
Let me know if this helps, or if I've misunderstood what you were trying to accomplish.
I am not 100% sure, but I believe that ngBindHtml will not help you in this case.
ngBindHtml is for displaying some "normal" HTML, but you want to display some Angular, magic HTML.
For that you need to $compile the HTML to something that is Angular-aware and link the compiled HTML to a scope.
I used the following approach (with apparently good results):
controller: function ($scope, $element, $compile) {
var html = createTmpl(angular.fromJson($scope.myOptions));
$scope.$watch('ngModel', function (newVal) {
var elem = angular.element(html); // Creating element
var linkingFn = $compile(elem); // Compiling element
linkingFn($scope); // Linking element
$element.html(''); // Removing previous content
$element.append(elem); // Inserting new content
// The above is purposedly explicit to highlight what is
// going on. It's moe concise equivalent would be:
//$element.html('').append($compile(html)($scope));
});
where createTmpl() is defined to take into account myOptions and return the appropriate template for creating a table with a header-row (based on the keys of myOptions) and data-rows with the properties defined as myOptions's values:
function createTmpl(options) {
// Construct the header-row
var html = '<tr>';
angular.forEach(options, function (value, key) {
html += '<th>' + key + '</th>';
});
html += '</tr>\n';
// Construct the data-rows
html += '<tr ng-repeat="user in ngModel">';
angular.forEach(options, function (value, key) {
html += '<td>{{user' + value + '}}</td>';
});
html += '</tr>\n';
// Return the template
return html;
}
See, also, this short demo.
Of course, this is for demonstration purposes only and does not handle everything a production-ready app should (e.g. accounting for errors, missing properties, changes in myOptions and whatnot).
UPDATE:
I had very strong competion, so I did a slight modification of the code above in order to support nested properties. E.g. given an object with the following structure:
user = {
name: 'ExpertSystem',
company: {
name: 'ExpertSystem S.A.',
ranking: 100
}
};
we can have the company name displayed in a column of our table, just by defining myOptions like this:
myOptions='{"Company name": "company.name"}
I have a table that allows the column headers to be re-ordered by dragging and dropping. The column headers are bound to an angular model, except the last column that is always fixed. I am using jquery UI dragtable to handle the drag/drop aspect (wired up using a simple custom directive).
This worked well enough in Angular 1.0.8, but after upgrading to 1.2.2 sometimes the last fixed column moves to the middle of the table, and the angular model gets out-of-sync with the view.
Working jsfiddle using 1.0.8: http://jsfiddle.net/Lw5dG/
Broken jsfiddle using 1.2.2: http://jsfiddle.net/Lw5dG/2 (drag col3 all the way to the left to recreate and note that the UL element below the table shows the wrong column order)
(both contain exactly the same code, the only difference is the version of angular used)
View
<div ng-app="myapp" ng-controller="TestCtrl">
<table id="tab1">
<tr>
<th class="accept" drag-table ng-repeat="col in model.cols">{{col}}</th>
<th class="static">STATIC</th>
</tr>
</table>
<ul>
<li ng-repeat="col in model.cols">{{col}}</li>
</ul>
</div>
JS
var myapp = angular.module('myapp', []);
myapp.controller('TestCtrl', function ($scope) {
$scope.model = {};
$scope.model.cols = ['Col1', 'Col2', 'Col3'];
$scope.dragTableDone = function(){
$scope.model.cols = [];
$('#tab1 th:not(.static)').each(function () {
console.log('pushing ' + $(this).text());
$scope.model.cols.push($(this).text());
});
$scope.$apply();
};
});
myapp.directive('dragTable', function() {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch('$last', function (v) {
if (v) {
$(element).closest('table').dragtable({ maxMovingRows: 1, beforeStop:scope.$parent.dragTableDone, dragaccept: '.accept' });
}
});
}
};
});
$(function() {
$('table').dragtable();
});
I appreciate that I may not be doing this the angular way, so if you can suggest a better approach please let me know.