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">
Related
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.
What I am doing: I am creating two attribute directives: One moves an element to the left, and another centers an element on the page. See code below. Notice that I'm manipulating ng-style to set those css properties (moving the element to the left, and centering on the page).
The Problem:
When I use both directives on an element, the second directive's scope.style={} obviously overwrites the first one.
This means that I'm not able to apply both ng-style/attributes at the same time.
My question:
What is an easy way to instantiate the scope.style object so that I can use it in both directives without recreating the object?
I'm trying to find a simple solution that can be scaled easily for multiple element directives without writing tons of messy code. (I don't want to create a special controller inside every directive to accommodate sharing the scope.style object).
Please advise. Thanks a lot!
Here is the html:
The data is from a json file, but that's not important here.
<!-- The Element: -->
<started-box center-page keepleft screen="screen[3]"></started-box>
<!-- The Template: -->
<div id="{{screen.id}}" ng-style="$parent.style">
Some content goes here.
</div>
Here are the AngularJS code snippets:
// Two Attribute directives:
app.directive("centerPage", function () {
return function (scope, element, attrs) {
var winW = window.innerWidth;
var winH = window.innerHeight;
var boxW = 370;
var boxH = 385;
scope.style = {};
scope.style.left = (winW / 2) - (boxW / 2);
scope.style.top = (winH / 2) - (boxH / 2);
}
});
app.directive("keepleft", function () {
return function (scope, element, attrs) {
scope.style = {};
scope.style.cursor = 'default';
scope.style.transform = 'translateX(-60%)';
}
});
// Directive for the template:
app.directive("startedBox", [function () {
return {
restrict: "E",
replace: true,
scope: {
screen: '='
},
templateUrl: dir + 'templates/directives/started-box.html'
};
}]);
Create a style service and inject it into both directives. Services are excellent for sharing states between directives/controllers.
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}}>
First off, I know that's a heck of a title.
Ive recently taken over angular-tooltip and am attempting to build a custom tooltip for my main work project.
In my project, I have an ng-repeat directive that simply says
<div class="row-submenu-white" ng-repeat="merge in potentialMerges | limitTo: numPotentialMergesVisible" company-profile-tooltip="merge.preview"></div>
Using the instructions for the library, I defined a custom tooltip directive:
myApp.directive('companyProfileTooltip', ['$tooltip', ($tooltip) => {
return {
restrict: 'EA',
scope: { profile: '#companyProfileTooltip' },
link: (scope: ng.IScope, elem) => {
var tooltip = $tooltip({
target: elem,
scope: scope,
templateUrl: "/App/Private/Content/Common/company.profile.html",
tether: {
attachment: 'middle right',
targetAttachment: 'middle left',
offset: '0 10px'
}
});
$(elem).hover(() => {
tooltip.open();
}, () => {
tooltip.close();
});
}
};
}]);
Company.profile.html is simply:
<div>Hello, world!</div>
Now, if you notice, in the ng-repeat I have a limitTo filter. For each of those (inititally 3) merges work perfectly, where a <div>Hello, world!</div> tooltip is properly added.
Then I trigger the limitTo to limit to a greater number. Each repeated element after the initial 3 gives me the following error:
TypeError: context is undefined
if ( ( context.ownerDocument || context ) !== document ) {
The error is in jquery-2.1.1.js, which debugging appears to be hopelessly over my head.
What I can tell you is that the function being called for that line is
Sizzle.contains = function( context, elem ) {
// Set document vars if needed
if ( ( context.ownerDocument || context ) !== document ) {
setDocument( context );
}
return contains( context, elem );
};
With a call stack of
Sizzle</Sizzle.contains(context=Document 046f7364-fa8d-4e95-e131-fa26ae78d108, elem=div)jquery-2.1.1.js (line 1409)
.buildFragment(elems=["<div>\r\n Hello, world!\r\n</div>"], context=Document 046f7364-fa8d-4e95-e131-fa26ae78d108, scripts=false, selection=undefined)jquery-2.1.1.js (line 5123)
jQuery.parseHTML(data="<div>\r\n Hello, world!\r\n</div>", context=Document 046f7364-fa8d-4e95-e131-fa26ae78d108, keepScripts=true)jquery-2.1.1.js (line 8810)
jQuery.fn.init(selector="<div>\r\n Hello, world!\r\n</div>\r\n", context=undefined, rootjQuery=undefined)jquery-....2.1.js (line 221)
jQuery(selector="<div>\r\n Hello, world!\r\n</div>\r\n", context=undefined)jquery-2.1.1.js (line 76)
compile($compileNodes="<div>\r\n Hello, world!\r\n</div>\r\n", transcludeFn=undefined, maxPriority=undefined, ignoreDirective=undefined, previousCompileContext=undefined)angular.js (line 6812)
m()angular....min.js (line 2)
.link/<()app.js (line 262)
jQuery.event.special[orig].handle(event=Object { originalEvent=Event mouseover, type="mouseenter", timeStamp=0, more...})jquery-2.1.1.js (line 4739)
jQuery.event.dispatch(event=Object { originalEvent=Event mouseover, type="mouseenter", timeStamp=0, more...})jquery-2.1.1.js (line 4408)
jQuery.event.add/elemData.handle(e=mouseover clientX=980, clientY=403)jquery-2.1.1.js (line 4095)
App.js line 262 being the link function of the directive supplied above.
For the life of me, I cannot figure out what makes the context undefined in repeated elements that come after the initial limitTo is increased. What I can verify is that removing the limitTo filter causes the behavior to be fine in each element throughout. What I can also verify is that the initial 3 elements do not work if I set the initial limitTo value to 0 and increase it after.
Looking at the source code for limitTo leads me to believe that a new array is constructed each time the amount you're limiting to changes. As to my understanding, this should cause angular to remove all the DOM elements and then change them, but I cannot tell if that change would change affect this in any way.
I know that there's not much to work off of, but I am lost as to how to debug this and could appreciate any help, or if there's any behavior in ng-repeat that I'm not aware of that could explain this.
I would guess that elem isn't added to the dom "fast enough" when you update numPotentialMergesVisible.
Try the following:
myApp.directive('companyProfileTooltip', ['$tooltip', ($tooltip) => {
return {
restrict: 'EA',
scope: { profile: '#companyProfileTooltip' },
link: (scope: ng.IScope, elem) => {
var tooltip = $tooltip({
target: elem,
scope: scope,
templateUrl: "/App/Private/Content/Common/company.profile.html",
tether: {
attachment: 'middle right',
targetAttachment: 'middle left',
offset: '0 10px'
}
});
$timeout(()=> {
$(elem).hover(() => {
tooltip.open();
}, () => {
tooltip.close();
});
},1);
}
};
}]);
This way the hover setup method will be executed after the $scope variable value change has been handled.
Apparently, the issue was that the wrong data was being cached in the angular-tooltip library. The entire request for the template was being parsed, rather than just the content of it. The issue had nothing to do with ngRepeat; the first n-items before the limitTo would fire off a GET request because the template data had not yet populated the templateCache, but later on they would be trying to access the content of the entire request.
I have an input box which accepts the value for div width.
Using angular.js, How can you change div's width based on user input?
I have implemented following code after referring this fiddle.http://jsfiddle.net/311chaos/vUUf4/4/
Markup:
<input type="text" id="gcols" placeholder="Number of Columns" ng-model="cols" ng-change="flush()"/>
<div getWidth rowHeight=cols ng-repeat="div in divs">{{$index+1}} aaaaaa</div>
controller.js
var grid = angular.module('gridApp', []);
grid.controller('control', ['$scope', function ($scope) {
/* code for repeating divs based on input*/
$scope.divs = new Array();
$scope.create=function(){ //function invoked on button's ng-click
var a = $scope.cols;
for(i=0;i<a;i++)
{
$scope.divs.push(a);
}
alert($scope.divs);
};
}]);
grid.directive('getWidth', function () {
return {
restrict: "A",
scope: {
"rowHeight": '='
},
link: function (scope, element) {
scope.$watch("rowHeight", function (value) {
console.log(scope.rowHeight);
$(element).css('width', scope.rowHeight + "px");
}, false);
}
}
});
function appCtrl($scope) {
$scope.cols = 150; //just using same input value to check if working
}
UPDATE
My ultimate goal is to set width of that div. When I call inline CSS like following I achieve what I want.
<div get-width row-height="{{cols}}" ng-repeat="div in divs" style="width:{{cols}}px">{{$index+1}} aaaaaa</div>
But this wouldn't be always efficient if number of divs goes high. So, again question remains, how to do this via angular script?
Use ng-style="{width: cols + 'px'}", instead of style.
link to the docs: http://docs.angularjs.org/api/ng.directive:ngStyle
You have an error in yor angular syntax. You Should use
ng-repeat="div in divs track by $index"
Also you should use attrs.$observe instead of $watch.
Working demo (updated): http://jsfiddle.net/nidzix/UKHyL/40/