I'm using the infinite scroll technique in conjunction with ng-repeat. I want to display a preloader up until the directive has finished adding the items to the DOM. What would be the simplest way of achieving that?
Try this live DEMO I set up for reference.
It depends on your infinite scroll implementation. And for best answer you should set up a plunker or jsbin.
But what about just setting a loader and using ng-if directive to only show it while the item container is empty ?
Imagine we have a template like
<div id="data-container" when-scrolled="loadMore()">
<img ng-if="!items.length" ng-src="http://placehold.it/100x395&text=Loading">
<div class="item" ng-repeat="item in items">{{item.id}}</div>
<img ng-if="items.length && busy" ng-src="http://placehold.it/85x50&text=Loading">
</div>
Here when-scrolled is our infinite-scroll directive which just monitors the scroll position and calls the supplied handler when it is time to load more items.
app.directive('whenScrolled', function() {
return function(scope, element, attr) {
var containerNode = element[0];
element.bind('scroll', function() {
if (containerNode.scrollTop + containerNode.offsetHeight >= containerNode.scrollHeight) {
scope.$apply(attr.whenScrolled);
}
});
};
});
Handler is called when the scroll hits the bottom of the content area.
loadMore() method in the controller could be defined like this:
$scope.items = [];
$scope.busy = false;
$scope.loadMore = function() {
if (true === $scope.busy) {
return;
}
$scope.busy = true;
$timeout(function() {
var currentLength = $scope.items.length;
for (var i = currentLength; i < currentLength + 10; i++) {
$scope.items.push({id: i + 1});
}
$scope.busy = false;
}, 350); // Imitating the long remote request.
};
We first initialize the $scope.items and while it's length is 0 the loading image is shown in the template as it is shown while "!items.length" is true. When the first items are added to the collection preloader gets hidden.
Then there's a local loading image which could be replaced with a spinner or whatever you like. It is shown then the $scope.busy var is set to true and is hidden when it's false. We change the $scope.busy value at the start and end of the aync request. Timeouts are used here for simple of async request demo.
Related
I'm loading a Partial view which contains AngularJS code, using the code below:
http.post("/Admin/LoadPartial", {
path: "~/Views/Admin/Urchin/Modals/_ChangeHero.cshtml",
model: self.newID
}).then(function (res) {
//res.data is the .cshtml
var element = angular.element(res.data);
var modal = $compile(element)(scope);
self.newSlides.push({
"ID": self.newID,
"Data": self.newData,
"Modal": modal.html()
});
scope.$emit("ngRepeatFinished");
Notify.Show("Saul goodman", "notice");});
This is how I render the partial:
<div ng-repeat="item in h.newSlides"
ng-bind-html="item.Modal | to_trusted" id="Hey-{{item.ID}}"></div>
And the filter:
.filter('to_trusted', ['$sce', function ($sce) {
return function (text) {
return $sce.trustAsHtml(text);
};
}])
The problem:
The rendered partial loads as HTML, but it contains code like this:
<button id="bgProg-#Model" class="progress-button" ng-click="h.EditBG()">
where h is the controller that loaded the .cshtml, and no click event is bound to the button.
Any ideas as to where I'm doing things wrong are greatly appreciated.
Progress
Thank you #user1620220 for the response.
I added this right after Notify.Show(.. :
timeout(function () {
var what = document.getElementById("Hey-" + self.newID);
var element = angular.element(what);
var modal = $compile(element)(scope);
what.innerHTML = content;}, 0, false);
and still no bindings are happening.
You are using $compile to generate a compiled node, but then you are calling html() to convert the node back to a string. ng-bind-html then converts the string into an uncompiled node and adds it to the DOM.
Instead, just pass res.data to ng-bind-html, allow the digest loop to run, then compile the in-situ DOM node.
Edit: After reading your edit, it occurred to me you need to use the cloneAttachFn returned by $compile. Here is my new proposed solution:
HTML:
<div ng-repeat="item in h.newSlides">
<div id="Hey-{{item.ID}}"><!--place holder--></div>
</div>
JS:
var linkFn = $compile(res.data);
timeout(function () {
self.newSlides.forEach((slide) => {
var what = document.getElementById("Hey-" + slide.ID);
linkFn(scope,(clone) => {
what.parentNode.replaceChild(clone, what);
});
});
}, 0, false);
i am using the wysiwyg redactor in my angular project with the following directive i extended.
why does the element not display anything at focusIn nor does it update after focusOut, it's probably because of ngModel.$render().
how can i register the last event ngModel.$render() if it is only valid after focusIn was executed?
do the events have to be re-registered on focusIn/Out because i replace elements?
// focusin/out event for replacing div
function focusIn(e) {
//wysi redactor template
var tmpl = '<div ng-model=model validation="{{::scope.column.validation}}" placeholder="{{::scope.column.placeholder}}" class="wysi f12 form-control" contenteditable redactor';
if (element.hasClass('one-row'))
tmpl += ' ="{deniedTags: [\'br\'],enterKey: false,pastePlainText: true,linebreaks: true}" ng-class="one-row"></div>';
else
tmpl += '></div>';
var tmp = $compile(tmpl)(scope);
//var tmp = angular.element(tmpl);
// Resume the compilation phase after setting ngModel
element.replaceWith(tmp);
// put in timeout to avoid $digest collision. call render() to
// set the initial value.
$timeout(function() {
editor = element.redactor(options);
element.on('focusout', focusOut);
ngModel.$render();
//element.on('remove', function () {
// //console.log('redactor remove ' + scope.column);
// element.off('remove');
// element.redactor('core.destroy');
// //todo aow: maybe save value?
//});
}, 150);
}
//destroy redactor when losing focus
function focusOut(e) {
//for html render in read-only div
scope.readonlyContent = scope.column.content;
//destroy redactor
element.redactor('core.destroy');
//replace redactor with read-only div
element.replaceWith(this.template);
//$compile(element)(scope);
element.on('click', focusIn);
console.log('after settemplate');
}
ngModel.$render = function() {
if(angular.isDefined(editor)) {
$timeout(function() {
console.log('redactor render ' + scope.column);
//var ed = element.find('.wysi');
element.redactor('code.set', ngModel.$viewValue || '');
element.redactor('placeholder.toggle');
scope.redactorLoaded = true;
});
}
};
i am doing this so complicated because ng-model doesn't suppurt the proper rendering of html, it has to be in ng-bind-html to render properly so i have to use 2 different divs
please check out my plunker version.
i have solved it by putting the ngModel.$render() function in a separate directive as it originally was. my mistake
I have a list of ng-repeat elements that are filtered based on boolean vars in scope.
HTML:
<button ng-click="toggleSection(1)">Section 1</button>
<button ng-click="toggleSection(2)">Section 2</button>
<button ng-click="toggleSection(3)">Section 3</button>
<div id="$index" ng-repeat="section in sections track by $index | filterSections">
...
</div>
the filter:
myApp.filter('filterSections', function() {
return function(input) {
var returnArray = [];
$.each(input, function(index, bool) {
if (bool) returnArray.push(bool);
});
return returnArray;
}
})
and in my controller:
$scope.sections = [false, false, false];
$scope.toggleSection = function(n) {
$scope.sections[n] = $scope.sections[n] ? false : true;
}
However, I want the same toggleSection function to go to the position of the element that it shows, like...
$scope.toggleSection = function(n) {
$scope.sections[n] = $scope.sections[n] ? false : true;
window.scrollTo($("#" + n).position().top, 0);
}
But I can't do this because it takes time for the DOM to show the section that it's creating. Before that, the element (and its position) does not exist.
Setting a timeout would probably work, but that seems sloppy.
I suspect that I need to create some sort of callback or promise. Is that right?
Custom directive, promise or anything else, if your action (scroll) relies on the DOM (and it does), you cannot avoid to execute it after an angular digest cycle.
Hence the short way to do that is indeed with a timeout:
$scope.toggleSection = function(n) {
$scope.sections[n] = $scope.sections[n] ? false : true;
$timeout(function() {
window.scrollTo($("#" + n).position().top, 0);
}, 0, false);
}
Note the third argument of $timeout being false: this is because you do not need to $apply the scope (run another digest cycle) after the timeout execute its callback since it is jQuery only.
Again, scrolling before the DOM is updated does not make sense for a browser, so you cannot avoid it (yet you can hide it).
What is the right way to load data (json) with angular when scrolling down?
HTML
<div id="fixed" when-scrolled="loadMore()">
<ul>
<li ng-repeat="i in items">{{product.name}}</li>
</ul>
</div>
JS
app.controller("MainController", function($scope, $http){
var counter = 0;
$scope.products = [];
$scope.loadMore = function() {
for (var i = 0; i < 10; i++) {
$scope.products.push({name: counter});
counter += 10;
}
};
$scope.loadMore();
}]);
myApp.directive("whenScrolled", function(){
return{
restrict: 'A',
link: function(scope, elem, attrs){
// we get a list of elements of size 1 and need the first element
raw = elem[0];
// we load more elements when scrolled past a limit
elem.bind("scroll", function(){
if(raw.scrollTop+raw.offsetHeight+5 >= raw.scrollHeight){
scope.loading = true;
// we can give any function which loads more elements into the list
scope.$apply(attrs.whenScrolled);
}
});
}
}
});
Working Example:http://plnkr.co/edit/FBaxdekW1Ya04S7jfz0V?p=preview?
What we did is make a directive that binds to the scroll event of the window and calls a scope function to load the data.
Or you could use ngInfiniteScroll.
Virtual Scroll: Display only a small subset of the data that is in the viewport and keep changing the records as the user scrolls. It keeps the number of DOM elements constant hence boosting the performance of the application.
this article so helpful
https://medium.com/codetobe/learn-how-to-us-virtual-scrolling-in-angular-7-51158dcacbd4
When my application loads or fetches data I add an entry to an array called "loading"
I then have the following that displays in my status bar:
<span data-ng-repeat="load in loading">|</span>
It shows a vertical bar for every item loading.
Is there a way that I could also change my cursor so that when there's some loading
activity (when loading.length > 0) then the cursor changes to :
cursor:wait;
If you are using ui.router, you can use the $stateChangeStart, $stateChangeSuccess and $stateChangeError to trigger when and when not to show the loading cursor. If you are using routes, simply use $routeChangeStart, $routeChangeSuccess and $routeChangeError in replacement.
//loading controls
document.body.style.cursor='default';
$scope.$on('$stateChangeStart', function() {
document.body.style.cursor='wait';
});
$scope.$on('$stateChangeSuccess', function() {
document.body.style.cursor='default';
});
$scope.$on('$stateChangeError', function() {
document.body.style.cursor='default';
});
You can also use these state changes to trigger a loading icon.
//loading controls
$scope.isViewLoading = false;
document.body.style.cursor='default';
$scope.$on('$stateChangeStart', function() {
$scope.isViewLoading = true;
document.body.style.cursor='wait';
});
$scope.$on('$stateChangeSuccess', function() {
$scope.isViewLoading = false;
document.body.style.cursor='default';
});
$scope.$on('$stateChangeError', function() {
$scope.isViewLoading = false;
document.body.style.cursor='default';
});
<span class="spinner" ng-show="isViewLoading">Loading...</span>
You can use ng-style or ng-class
For example, create custom style:
$scope.state = 'wait';
$scope.mySyle = {
'cursor': state
}
Now, we can change our $scope.state during the time and out style will change respectively.
For ng-class - the same thing, just create style into css file and switch ng-class value.
Here is a references:
ng-class - use when the set of CSS styles is static/known ahead of time
ng-style - use when you can't define a CSS class because the style values may change dynamically. Think programmable control of the style values.
(took from THIS POST)