AngularJS: Scroll to end of element after applying DOM changes - angularjs

I have a simple list of items. I want to be able to scroll to the bottom of the element displaying the items whenever I add more items. I understood there is no way of hooking to the end of the $apply() function, so what might be my solution?
Here is a jsfiddle to illustrate my problem. after adding enough items, the ul element doesn't scroll to the bottom...

There's the very awesome angularjs-scroll-glue available, which does exactly what you want.
All you need to do is to apply the scroll-glue directive to your container element, and you get exactly what you're looking for.
There's also a demo available.

Another valid solution to this is using $timeout. Using a timeout value of 0, angular will wait until the DOM is rendered before calling the function you pass to $timeout. So, after you add an element to the list, you can use this to wait for your new element to be added to the DOM before scrolling to the bottom.
Like #Mark Coleman's solution, this won't require any extra external libraries.
var myApp = angular.module('myApp', []);
function MyCtrl($scope, $timeout) {
$scope.list = ["item 1", "item 2", "item 3", "item 4", "item 5"];
$scope.add = function() {
$scope.list.push("new item");
$timeout(function() {
var scroller = document.getElementById("autoscroll");
scroller.scrollTop = scroller.scrollHeight;
}, 0, false);
}
}
ul {
height: 150px;
overflow: scroll;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<button ng-click="add()">Add</button>
<ul id="autoscroll">
<li ng-repeat="item in list">{{item}}</li>
</ul>
</div>
</div>

A simple working example (no need for plugins or directives)...
.controller('Controller', function($scope, $anchorScroll, $location, $timeout) {
$scope.func = function(data) {
// some data appending here...
$timeout(function() {
$location.hash('end');
$anchorScroll();
})
}
})
The trick that did it for me was wrapping the anchorScroll command with $timeout, that way the scope was resolved and it automatically shifted to an element at the end of the page.

You could create a simple directive that bind a click handler that scrolls the <ul> to the bottom each time.
myApp.directive("scrollBottom", function(){
return {
link: function(scope, element, attr){
var $id= $("#" + attr.scrollBottom);
$(element).on("click", function(){
$id.scrollTop($id[0].scrollHeight);
});
}
}
});
example on jsfiddle

You can use the AnchorScroll.. here the documentation: https://docs.angularjs.org/api/ng/service/$anchorScroll

You can achieve this using angularjs custom directory.
example :
<ul style="overflow: auto; max-height: 160px;" id="promptAnswerBlock">
<li ng-repeat="obj in objectKist track by $index" on-finish-render="ngRepeatFinished">
app.directive('onFinishRender', function($timeout) {
return {
restrict : 'A',
link : function(scope, element, attr) {
if (scope.$last === true) {
$timeout(function() {
$('#promptAnswerBlock').scrollTop($('#promptAnswerBlock')[0].scrollHeight + 150);
});
}
}
}
});
</li>

Related

AngularJS - Callback after ng-repeat update

I got some trouble understanding how I make a callback after I've updated an ng-repeat. I basically want to be able to make a callback function after my updates to my ng-repeat has been finished. Currently have this:
var app = angular.module('myApp', []);
app.directive('onLastRepeat', function() {
return function(scope, element, attrs) {
if (scope.$first)
console.log("ng-repeat starting - Index: " + scope.$index)
if (scope.$last) setTimeout(function(){
console.log("ng-rpeat finished - Index: " + scope.$index);
}, 1);
};
});
app.controller('MyController', function($scope) {
$scope.data = [1,2,3,4,5,6,7,8,9,10,12,12,13,14,15,16,17,18,19,20];
$scope.buttonClicked = function() {
console.log('Btn clicked');
$scope.randomItems = getRandomItems(this.data.length);
};
});
HTML
<div ng-app="myApp">
<div ng-controller="MyController">
<button ng-click="buttonClicked()">Highlight random</button>
<ul class="item" >
<li ng-repeat="item in data" ng-class="{highlight: randomItems.indexOf($index) > -1}" on-last-repeat>{{ item }} </li>
</ul>
</div>
</div>
Link to fiddle: https://jsfiddle.net/hbhodgm3/
So how the "app" works is that it lists the content of the data-array then when you click the "highlight"-button it randomly highlights 2 in the list. So my problem is that I want to have a callback function for when the highlighting/DOM-render is done. I found a way to do this for the initial ng-repeat with $scope.first and $scope.last to check when ng-repeat is done, but doesn't seem to work with the highlighting.
Hope I managed to explain the problem,
Thanks in advance.
See $q and Promises for a better understanding of how to work with the asynchronous nature of angular.
Presuming getRandomItems(this.data.length); is an API call that could take seconds to perform:
asyncItems(this.data.length).then(function(randoms){
$scope.randomItems = randoms;
//handle post rendering callback
});
function asyncItems(length) {
var deferred = $q.defer();
var items = getRandomItems(length);
if (items){
deferred.resolve(items);
}
else {
//no items :(
deferred.reject([]);
}
return deferred.promise;
}

Hide / Show list with angular

I have a DL list that is rendered at server side:
<dl>
<dt>dt 01</dt>
<dd>dd 01</dd>
<dt>dt 02</dt>
<dd>dd 02</dd>
</dl>
I would like to hide / show a DD when a DT is clicked. But I also need to change both the dt and the dd class when this happens.
Can I do this with angular? Do I need, or should I use, a controller for this?
Here's a codepen example to get you started. It's not a complete solution but should point you in right direction.
and here's the HTML:
<body ng-controller="MainCtrl">
<dl toggle-desc>
<dt>dt 01</dt>
<dd>dd 01</dd>
<dt>dt 02</dt>
<dd>dd 02</dd>
</dl>
</body>
And JS:
angular.module('myApp', []).controller('MainCtrl', function($scope) {
}).directive('toggleDesc', function() {
return {
restrict: 'A',
link: function(scope, element) {
var dtList = element.find('dt');
dtList.bind('click', function(evt) {
//TODO: Hide/show next sibling, change class names etc.
});
}
};
});

How do I control the execution flow so the ng-repeat completes before the parent directive?

I'd like extend-item directive to run after ng-repeat is rendered in DOM.
In the example below the ^ is added only to the static ul elements, since the dynamic ul elements are not yet generated by ng-repeat yet. See code in Plunker
How do I control the execution flow so the ng-repeat completes before the parent directive?
<ul id="nav" ng-controller="AppController as vm" extend-item>
<li ng-repeat="group in vm.groups">
<span>{{group.name}}</span>
<ul>
<li ng-repeat="item in vm.items">
<span>{{item.name}}</span>
</li>
</ul>
</li>
<!-- Static -->
<li>
<span>Static Group 10</span>
<ul>
<li><span>Static 11</span>
</li>
</ul>
</li>
</ul>
(function() {
'use strict';
angular.module('app')
.directive('extendItem', extendItem);
extendItem.$inject = ['$compile'];
/* #ngInject */
function extendItem($compile) {
var directive = {
restrict: 'A',
link: link
};
return directive;
function link(scope, element, attrs, ctrls) {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
}
}
})();
(function() {
'use strict';
angular.module('app')
.controller('AppController', AppController);
AppController.$inject = ['$scope'];
/* #ngInject */
function AppController($scope) {
var vm = this;
vm.groups = [{
id: 1,
name: 'Group 1'
}, {
id: 2,
name: 'Group 2'
}];
vm.items = [{
id: 1,
name: 'Item 1'
}, {
id: 2,
name: 'Item 2'
}];
activate();
///////////////////////////////
function activate() {
console.log('AppController');
}
}
})();
The easiest way to do that is use $timeout before executing your code: this will wait for the current digest cycle to be done and the DOM to be refreshed.
http://plnkr.co/edit/KVlzZHHwa3ECxteJBvD8?p=preview
function link(scope, element, attrs, ctrls) {
$timeout(function() {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
});
}
The problem with $timeout is that it will only work the first time your DOM is evaluated and relies on your model never changing. If your model changes (i.e, you add groups), the new groups will not have the ^ appended.
Here is a plunk that demonstrates this problem.
I would instead use css (if you are using a modern browser)
<style>
ul[extend-item] > li::after {
content: '^'
}
</style>
In general, any time you find yourself wanting to modify the DOM after ng-repeat "completes" or "is rendered", you are thinking about it "wrong" in the Angular sense. ng-repeat never "completes". It is always watching the model and can potentially add or remove elements based on changes in the model. Using $timeout does not guarantee that the DOM will not be modified at some time in the future.
You might want to also investigate using ng-repeat-start and ng-repeat-end for this particular use case, especially if the intent is to add more than just simple content after each list element.
From the docs:
To repeat a series of elements instead of just one parent element,
ngRepeat (as well as other ng directives) supports extending the range
of the repeater by defining explicit start and end points by using
ng-repeat-start and ng-repeat-end respectively. The ng-repeat-start
directive works the same as ng-repeat, but will repeat all the HTML
code (including the tag it's defined on) up to and including the
ending HTML tag where ng-repeat-end is placed.

Angularjs - Hide content until DOM loaded

I am having an issue in Angularjs where there is a flicker in my HTML before my data comes back from the server.
Here is a video demonstrating the issue: http://youtu.be/husTG3dMFOM - notice the #| and the gray area to the right.
I have tried ngCloak with no success (although ngCloak does prevent the brackets from appearing as promised) and am wondering the best way to hide content until the HTML has been populated by Angular.
I got it to work with this code in my controller:
var caseCtrl = function($scope, $http, $routeParams) {
$('#caseWrap').hide(); // hides when triggered using jQuery
var id = $routeParams.caseId;
$http({method: 'GET', url: '/v1/cases/' + id}).
success(function(data, status, headers, config) {
$scope.caseData = data;
$('#caseWrap').show(); // shows using jQuery after server returns data
}).
error(function(data, status, headers, config) {
console.log('getCase Error', arguments);
});
}
...but I have heard time and time again not to manipulate the DOM from a controller. My question is how can I achieve this using a directive? In other words, how can I hide the element that a directive is attached to until all content is loaded from the server?
In your CSS add:
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
and just add a "ng-cloak" attribute to your div like here:
<div id="template1" ng-cloak>{{scoped_var}}<div>
doc: https://docs.angularjs.org/api/ng/directive/ngCloak
On your caseWrap element, put ng-show="contentLoaded" and then where you currently have $('#caseWrap').show(); put $scope.contentLoaded = true;
If the caseWrap element is outside this controller, you can do the same kind of thing using either $rootScope or events.
Add the following to your CSS:
[ng\:cloak],[ng-cloak],.ng-cloak{display:none !important}
The compiling of your angular templates isn't happening fast enough.
UPDATE
You should not do DOM manipulation in your controller. There are two thing you can do...
1. You can intercept changes to the value within the scope of the controller via a directive! In your case, create a directive as an attribute that is assigned the property you want to watch. In your case, it would be caseData. If casedata is falsey, hide it. Otherwise, show it.
A simpler way is just use ngShow='casedata'.
Code
var myApp = angular.module('myApp', []);
myApp.controller("caseCtrl", function ($scope, $http, $routeParams, $timeout) {
$scope.caseData = null;
//mimic a delay in getting the data from $http
$timeout(function () {
$scope.caseData = 'hey!';
}, 1000);
})
.directive('showHide', function () {
return {
link: function (scope, element, attributes, controller) {
scope.$watch(attributes.showHide, function (v) {
if (v) {
element.show();
} else {
element.hide();
}
});
}
};
});
HTML
<div ng-controller='caseCtrl' show-hide='caseData'>using directive</div>
<div ng-controller='caseCtrl' ng-show='caseData'>using ngShow</div>
JSFIDDLE:http://jsfiddle.net/mac1175/zzwBS/
Since you asked for a directive, try this.
.directive('showOnLoad', function() {
return {
restrict: 'A',
link: function($scope,elem,attrs) {
elem.hide();
$scope.$on('show', function() {
elem.show();
});
}
}
});
Stick (show-on-load) in your element, and in your controller inject $rootScope, and use this broadcast event when the html has loaded.
$rootScope.$broadcast('show');
I have used Zack's response to create a 'loading' directive, which might be useful to some people.
Template:
<script id="ll-loading.html" type="text/ng-template">
<div layout='column' layout-align='center center'>
<md-progress-circular md-mode="indeterminate" value="" md-diameter="52"></md-progress-circular>
</div>
</script>
Directive:
directives.directive('loading', function() {
return {
restrict: 'E',
template: 'll-loading.html',
link: function($scope,elem,attrs) {
elem.show();
$scope.$on('loaded', function() {
console.log("loaded: ");
elem.hide();
});
}
}
});
This example uses angular-material in the html
The accepted answer didn't work for me. I had some elements that had ng-show directives and the elements would still show momentarily even with the ng-cloak. It appears that the ng-cloak was resolved before the ng-show returned false. Adding the ng-hide class to my elements fixed my issue.

AngularJS input with focus kills ng-repeat filter of list

Obviously this is caused by me being new to AngularJS, but I don't know what is the problem.
Basically, I have a list of items and an input control for filtering the list that is located in a pop out side drawer.
That works perfectly until I added a directive to set focus to that input control when it becomes visible. Then the focus works, but the filter stops working. No errors. Removing focus="{{open}}" from the markup makes the filter work.
The focus method was taken from this StackOverflow post:
How to set focus on input field?
Here is the code...
/* impersonate.html */
<section class="impersonate">
<div header></div>
<ul>
<li ng-repeat="item in items | filter:search">{{item.name}}</li>
</ul>
<div class="handle handle-right icon-search" tap="toggle()"></div>
<div class="drawer drawer-right"
ng-class="{expanded: open, collapsed: !open}">
Search<br />
<input class="SearchBox" ng-model="search.name"
focus="{{open}}" type="text">
</div>
</section>
// impersonateController.js
angular
.module('sales')
.controller(
'ImpersonateController',
[
'$scope',
function($scope) {
$scope.open = false;
$scope.toggle = function () {
$scope.open = !$scope.open;
}
}]
);
// app.js
angular
.module('myApp')
.directive('focus', function($timeout) {
return {
scope: { trigger: '#focus' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === "true") {
console.log('trigger',value);
$timeout(function() {
element[0].focus();
});
}
});
}
};
})
Any assistance would be greatly appreciated!
Thanks!
Thad
The focus directive uses an isolated scope.
scope: { trigger: '#focus' },
So, by adding the directive to the input-tag, ng-model="search.name" no longer points to ImpersonateController but to this new isolated scope.
Instead try:
ng-model="$parent.search.name"
demo: http://jsbin.com/ogexem/3/
P.s.: next time, please try to post copyable code. I had to make quite a lot of assumptions of how all this should be wired up.

Resources