Load Data while Scrolling Page Down with AngularJS - angularjs

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

Related

AngularJS directive with ng-show not toggling with change in variable

I'm trying to create a simple html5 video playlist app. I've got an overlay div on top of the html5 video that should appear/disappear when stopping and starting the video.
I've got ng-show and a variable to trigger it, but it's not changing when I look using ng-inspector.
My events might not be quite correct, either - but I can't seem to find much information on putting events on different elements within the same directive. Is this a clue that I should break this up into multiple directives?
(function() {
'use strict';
angular
.module('app')
.controller('campaignController', campaignController)
.directive('myVideo', myvideo);
function campaignController($log,Campaign) {
var vm = this;
vm.overlay = true;
Campaign.getCampaign().success(function(data) {
vm.campaign = data[0];
vm.item = vm.campaign.videos[0];
});
vm.select = function(item) {
vm.item = item;
};
vm.isActive = function(item) {
return vm.item === item;
};
};
function myvideo() {
return {
restrict: 'E',
template: ['<div class="video-overlay" ng-show="vm.overlay">',
'<p>{{ vm.campaign.name}}</p>',
'<img class="start" src="play.png">',
'</div>',
'<video class="video1" controls ng-src="{{ vm.item.video_mp4_url | trusted }}" type="video/mp4"></source>',
'</video>' ].join(''),
link: function(scope, element, attrs) {
scope.video = angular.element(document.getElementsByClassName("video1")[0]);
scope.startbutton = angular.element(document.getElementsByClassName("start")[0]);
scope.startbutton.on('click', function() {
scope.vm.overlay = false;
scope.video[0].play();
});
scope.video.on('click', function() {
scope.video[0].pause();
scope.vm.overlay = true;
});
}
};
}
})();
From my personal experience angular expression evaluation does not work as javascript. so try ng-show="vm.overlay==true".
Furthermore you bind click using native javascript.
Either don't do that and use ng-click or call scope.$apply() in the click event t callbackas last intruction (even though i'm not sure if it's really important).

Using ng-show directive to toggle multiple elements?

Say I've got a <nav> element with three buttons, and an <article> containing three <section> elements. I want a user to be able to click a button, which toggles the display of all of the <section> elements in such a way that only the one relevant <section> is shown.
I'm new to AngularJS and trying to figure out if theres a more Angularly way of achieving this than by giving the nav elements ng-click attributes and the section elements the ng-show attribute. It feels very imperative.
My HTML would look something like this:
<nav>
<div ng-click="showRedSection()"></div>
<div ng-click="showBlueSection()"></div> //etc.
<nav>
<article>
<section ng-show="redSectionShown">
// content; etc.
</section>
//etc.
</article>
Am I on the right track, or is there an approach that makes this more declarative?
What you are going is correct.
Instead of needing a function to toggle the value on click you could do redSectionShown = !!redSectionShown
Or if you have a lot of sections and other data you want to store create a service to store the visible state of the regions and create directive that can toggle the values or use the values to hide elements.
The second approach reduces your $scope pollution.
.service('ViewableRegions', function() {
var viewablePropertiesMap = {};
this.getValue = function(value) {
return viewablePropertiesMap[value];
}
this.setValue = function(valueToUpdate, value) {
viewablePropertiesMap[valueToUpdate] = value
}
})
Directive to toggle region visability
.directive('regionToggler', function(ViewableRegions) {
return {
restrict: 'A',
compile: function($element, $attrs) {
var directiveName = this.name;
var valueToUpdate = attrs[directiveName];
return function(scope, element, attrs) {
element.on('click', function() {
var currentValue = ViewableRegions.getValue(ViewableRegions);
ViewableRegions.setValue(valueToUpdate, !!currentValue);
scope.$apply();
})
};
}
};
}
);
Directive to display the regions
.directive('regionDisplayer', function(ViewableRegions) {
return {
restrict: 'A',
compile: function($element, $attrs) {
var directiveName = this.name;
var valueToUpdate = attrs[directiveName];
return function(scope, element, attrs) {
scope.$watch(
function() {
return ViewableRegions.getValue(ViewableRegions);
},
function(newVal) {
if (newVal) {
element[0].style.display = "none";
} else {
element[0].style.display = "";
}
}
)
};
}
};
}
);
HTML Uasge
//etc.
<article>
<section region-displayer="redSectionShown">
// content; etc.
</section>
//etc.
</article>
I would actually recommend trying to use ng-hide as opposed to ng-show. These directives complete the same task most of the time,but ng-hide defaults to a hidden state, where you can then setup parameters in which the information will be shown. I ran into a similar problem with an app I was working on in angular, and using ng-hide seemed to help simplify the code in my view.
You could try something like:
<div ng-click="sectionToShow=1"></div>
<div ng-click="sectionToShow=2"></div>
<article>
<section ng-show="sectionToShow==1"></section>
</article>
I haven't tested this, but there's no reason it shouldn't work. That way, you will automatically only ever show the correct section, and all other sections will auto hide themselves.

AngularJS - preloader for ngRepeat

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.

Angular.js ng-repeat not updating the dom elements when i destroy the scope object

I'm trying to delete elements in the ng-repeat from the controller for when a user scrolls down the screen to limit the amount of dom elements in view.
Heres what i have tried
$scope.someStuff = someobject..
delete $scope.friendsViewObject[SomeIndex];
And no success. If I delete the elements in the object alone the dom won't update.
And this is the directive that would call the controller.
<div myDirective="loadMyController()" ></div>
.directive('myDirective', function() {
return function(scope, elm, attr) {
var raw = elm[0];
elm.bind('scroll', function() {
if (((raw.scrollHeight - raw.offsetHeight) - raw.scrollTop) < 10 ) {
scope.$apply(attr.friendsWhenScrolled);
}
});
}
});
Instead of $apply on the attribute, when you change the DOM and you want angular to know about it, wrap the change in $apply.
So, your code should look something like this:
.directive('myDirective', function() {
return function(scope, elm, attr) {
var raw = elm[0];
elm.bind('scroll', function() {
if (((raw.scrollHeight - raw.offsetHeight) - raw.scrollTop) < 10 ) {
scope.$apply(function() {
// delete here
});
}
});
}
});
OK i discovered the problem..
I think this might be an angular.js bug with the ng-repeat..
When i destroy elements in the scopes array the actual data is removed from view BUT the dom elements are still there.. It looks like the ng-repeat goes off array.length property without any pre checking.. so it gets all the array counting wrong.. So i had to use the method shift() to re-arrange all the elements..
Solution:
Change this
delete $scope.friendsViewObject[SomeIndex];
To This
$scope.friendsViewObject.shift()

AngularJS: ng-repeat list is not updated when a model element is spliced from the model array

I have two controllers and share data between them with an app.factory function.
The first controller adds a widget in the model array (pluginsDisplayed) when a link is clicked. The widget is pushed into the array and this change is reflected into the view (that uses ng-repeat to show the array content):
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
The widget is built upon three directives, k2plugin, remove and resize. The remove directive adds a span to the template of the k2plugin directive. When said span is clicked, the right element into the shared array is deleted with Array.splice(). The shared array is correctly updated, but the change is not reflected in the view. However, when another element is added, after the remove, the view is refreshed correctly and the previously-deleted element is not shown.
What am I getting wrong? Could you explain me why this doesn't work?
Is there a better way to do what I'm trying to do with AngularJS?
This is my index.html:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js">
</script>
<script src="main.js"></script>
</head>
<body>
<div ng-app="livePlugins">
<div ng-controller="pluginlistctrl">
<span>Add one of {{pluginList.length}} plugins</span>
<li ng-repeat="plugin in pluginList">
<span>{{plugin.name}}</span>
</li>
</div>
<div ng-controller="k2ctrl">
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
</div>
</div>
</body>
</html>
This is my main.js:
var app = angular.module ("livePlugins",[]);
app.factory('Data', function () {
return {pluginsDisplayed: []};
});
app.controller ("pluginlistctrl", function ($scope, Data) {
$scope.pluginList = [{name: "plugin1"}, {name:"plugin2"}, {name:"plugin3"}];
$scope.add = function () {
console.log ("Called add on", this.plugin.name, this.pluginList);
var newPlugin = {};
newPlugin.id = this.plugin.name + '_' + (new Date()).getTime();
newPlugin.name = this.plugin.name;
Data.pluginsDisplayed.push (newPlugin);
}
})
app.controller ("k2ctrl", function ($scope, Data) {
$scope.pluginsDisplayed = Data.pluginsDisplayed;
$scope.remove = function (element) {
console.log ("Called remove on ", this.pluginid, element);
var len = $scope.pluginsDisplayed.length;
var index = -1;
// Find the element in the array
for (var i = 0; i < len; i += 1) {
if ($scope.pluginsDisplayed[i].id === this.pluginid) {
index = i;
break;
}
}
// Remove the element
if (index !== -1) {
console.log ("removing the element from the array, index: ", index);
$scope.pluginsDisplayed.splice(index,1);
}
}
$scope.resize = function () {
console.log ("Called resize on ", this.pluginid);
}
})
app.directive("k2plugin", function () {
return {
restrict: "A",
scope: true,
link: function (scope, elements, attrs) {
console.log ("creating plugin");
// This won't work immediately. Attribute pluginname will be undefined
// as soon as this is called.
scope.pluginname = "Loading...";
scope.pluginid = attrs.pluginid;
// Observe changes to interpolated attribute
attrs.$observe('pluginname', function(value) {
console.log('pluginname has changed value to ' + value);
scope.pluginname = attrs.pluginname;
});
// Observe changes to interpolated attribute
attrs.$observe('pluginid', function(value) {
console.log('pluginid has changed value to ' + value);
scope.pluginid = attrs.pluginid;
});
},
template: "<div>{{pluginname}} <span resize>_</span> <span remove>X</span>" +
"<div>Plugin DIV</div>" +
"</div>",
replace: true
};
});
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
})
};
});
Whenever you do some form of operation outside of AngularJS, such as doing an Ajax call with jQuery, or binding an event to an element like you have here you need to let AngularJS know to update itself. Here is the code change you need to do:
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
scope.$apply();
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
scope.$apply();
})
};
});
Here is the documentation on it: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
If you add a $scope.$apply(); right after $scope.pluginsDisplayed.splice(index,1); then it works.
I am not sure why this is happening, but basically when AngularJS doesn't know that the $scope has changed, it requires to call $apply manually. I am also new to AngularJS so cannot explain this better. I need too look more into it.
I found this awesome article that explains it quite properly.
Note: I think it might be better to use ng-click (docs) rather than binding to "mousedown". I wrote a simple app here (http://avinash.me/losh, source http://github.com/hardfire/losh) based on AngularJS. It is not very clean, but it might be of help.
I had the same issue. The problem was because 'ng-controller' was defined twice (in routing and also in the HTML).
Remove "track by index" from the ng-repeat and it would refresh the DOM
There's an easy way to do that. Very easy. Since I noticed that
$scope.yourModel = [];
removes all $scope.yourModel array list you can do like this
function deleteAnObjectByKey(objects, key) {
var clonedObjects = Object.assign({}, objects);
for (var x in clonedObjects)
if (clonedObjects.hasOwnProperty(x))
if (clonedObjects[x].id == key)
delete clonedObjects[x];
$scope.yourModel = clonedObjects;
}
The $scope.yourModel will be updated with the clonedObjects.
Hope that helps.

Resources