Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 23, 2013 13:04
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save bennadel/6062213 to your computer and use it in GitHub Desktop.
Save bennadel/6062213 to your computer and use it in GitHub Desktop.
Lazy Loading Image With AngularJS
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>
Lazy Loading Images With AngularJS
</title>
<style type="text/css">
/* Add "click" styles because newer releases of AngularJS don't seem to add the HREF value. */
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
a.box {
background-color: #FAFAFA ;
border: 1px solid #CCCCCC ;
display: block ;
height: 200px ;
line-height: 200px ;
text-align: center ;
width: 684px ;
}
ul.photos {
list-style-type: none ;
margin: 16px 0px 16px 0px ;
padding: 0px 0px 0px 0px ;
width: 700px ;
}
ul.photos:after {
content: "" ;
clear: both ;
display: block ;
height: 0px ;
}
li.photo {
background-color: #FAFAFA ;
border: 1px solid #CCCCCC ;
border-radius: 4px 4px 4px 4px ;
float: left ;
margin: 0px 10px 10px 0px ;
padding: 5px 5px 5px 5px ;
}
li.photo img {
border: 1px solid #EEEEEE ;
border-radius: 3px 3px 3px 3px ;
display: block ;
}
img[ bn-lazy-src ] {
background-image: url( "./checkered.png" ) ;
}
</style>
</head>
<body>
<h1>
Lazy Loading Images With AngularJS
</h1>
<p>
You have {{ photos.size }} photos in your set.
<a ng-click="rebuildSet()">Rebuild set</a>.
<a ng-click="changeSource()">Change src</a>.
<a ng-click="clearPhotos()">Clear</a>.
</p>
<a ng-show="isBoxVisible" ng-click="hideBox()" class="box">
This is a big thing that may change,
causing the DOCUMENT HEIGHT to change.
</a>
<ul class="photos">
<li ng-repeat="photo in photos" class="photo">
<img
bn-lazy-src="{{ photo.src }}"
width="150"
height="150"
alt="Christina Cox"
/>
</li>
</ul>
<!-- Load scripts. -->
<script
type="text/javascript"
src="../../vendor/jquery/jquery-2.0.3.min.js">
</script>
<script
type="text/javascript"
src="../../vendor/angularjs/angular-1.0.7.min.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// I flag the visibility of the big box.
$scope.isBoxVisible = true;
// Build up a large set of images, all with unique
// SRC values so that the browser cannot cache them.
$scope.photos = buildPhotoSet( 200 );
// ---
// PUBLIC METHODS.
// ---
// I change the SRC values of the existing photo set
// in order to examine how changes to source will
// affect rendered / non-rendered images.
$scope.changeSource = function() {
var now = ( new Date() ).getTime();
// Update all SRC attribute to point to "1.jpg".
for ( var i = 0 ; i < $scope.photos.length ; i++ ) {
var photo = $scope.photos[ i ];
photo.src = photo.src.replace( /\d\./i, "1." );
}
};
// I clear the current photo set.
$scope.clearPhotos = function() {
$scope.photos = [];
};
// I hide the big box, allowing the document to change
// its dimensions (and possibly show more images than
// were visible beforehand).
$scope.hideBox = function() {
$scope.isBoxVisible = false;
};
// I rebuild the entire photo set.
$scope.rebuildSet = function() {
$scope.photos = buildPhotoSet( 20 );
};
// ---
// PRIVATE METHODS.
// ---
// I return a photo set of the given size. Each photo
// will have a unique SRC value.
function buildPhotoSet( size ) {
var photos = [];
var now = ( new Date() ).getTime();
for ( var i = 0 ; i < size ; i++ ) {
var index = ( ( i % 3 ) + 1 );
var version = ( now + i );
photos.push({
id: ( i + 1 ),
src: ( "christina-cox-" + index + ".jpg?v=" + version )
});
}
return( photos );
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I lazily load the images, when they come into view.
app.directive(
"bnLazySrc",
function( $window, $document ) {
// I manage all the images that are currently being
// monitored on the page for lazy loading.
var lazyLoader = (function() {
// I maintain a list of images that lazy-loading
// and have yet to be rendered.
var images = [];
// I define the render timer for the lazy loading
// images to that the DOM-querying (for offsets)
// is chunked in groups.
var renderTimer = null;
var renderDelay = 100;
// I cache the window element as a jQuery reference.
var win = $( $window );
// I cache the document document height so that
// we can respond to changes in the height due to
// dynamic content.
var doc = $document;
var documentHeight = doc.height();
var documentTimer = null;
var documentDelay = 2000;
// I determine if the window dimension events
// (ie. resize, scroll) are currenlty being
// monitored for changes.
var isWatchingWindow = false;
// ---
// PUBLIC METHODS.
// ---
// I start monitoring the given image for visibility
// and then render it when necessary.
function addImage( image ) {
images.push( image );
if ( ! renderTimer ) {
startRenderTimer();
}
if ( ! isWatchingWindow ) {
startWatchingWindow();
}
}
// I remove the given image from the render queue.
function removeImage( image ) {
// Remove the given image from the render queue.
for ( var i = 0 ; i < images.length ; i++ ) {
if ( images[ i ] === image ) {
images.splice( i, 1 );
break;
}
}
// If removing the given image has cleared the
// render queue, then we can stop monitoring
// the window and the image queue.
if ( ! images.length ) {
clearRenderTimer();
stopWatchingWindow();
}
}
// ---
// PRIVATE METHODS.
// ---
// I check the document height to see if it's changed.
function checkDocumentHeight() {
// If the render time is currently active, then
// don't bother getting the document height -
// it won't actually do anything.
if ( renderTimer ) {
return;
}
var currentDocumentHeight = doc.height();
// If the height has not changed, then ignore -
// no more images could have come into view.
if ( currentDocumentHeight === documentHeight ) {
return;
}
// Cache the new document height.
documentHeight = currentDocumentHeight;
startRenderTimer();
}
// I check the lazy-load images that have yet to
// be rendered.
function checkImages() {
// Log here so we can see how often this
// gets called during page activity.
console.log( "Checking for visible images..." );
var visible = [];
var hidden = [];
// Determine the window dimensions.
var windowHeight = win.height();
var scrollTop = win.scrollTop();
// Calculate the viewport offsets.
var topFoldOffset = scrollTop;
var bottomFoldOffset = ( topFoldOffset + windowHeight );
// Query the DOM for layout and seperate the
// images into two different categories: those
// that are now in the viewport and those that
// still remain hidden.
for ( var i = 0 ; i < images.length ; i++ ) {
var image = images[ i ];
if ( image.isVisible( topFoldOffset, bottomFoldOffset ) ) {
visible.push( image );
} else {
hidden.push( image );
}
}
// Update the DOM with new image source values.
for ( var i = 0 ; i < visible.length ; i++ ) {
visible[ i ].render();
}
// Keep the still-hidden images as the new
// image queue to be monitored.
images = hidden;
// Clear the render timer so that it can be set
// again in response to window changes.
clearRenderTimer();
// If we've rendered all the images, then stop
// monitoring the window for changes.
if ( ! images.length ) {
stopWatchingWindow();
}
}
// I clear the render timer so that we can easily
// check to see if the timer is running.
function clearRenderTimer() {
clearTimeout( renderTimer );
renderTimer = null;
}
// I start the render time, allowing more images to
// be added to the images queue before the render
// action is executed.
function startRenderTimer() {
renderTimer = setTimeout( checkImages, renderDelay );
}
// I start watching the window for changes in dimension.
function startWatchingWindow() {
isWatchingWindow = true;
// Listen for window changes.
win.on( "resize.bnLazySrc", windowChanged );
win.on( "scroll.bnLazySrc", windowChanged );
// Set up a timer to watch for document-height changes.
documentTimer = setInterval( checkDocumentHeight, documentDelay );
}
// I stop watching the window for changes in dimension.
function stopWatchingWindow() {
isWatchingWindow = false;
// Stop watching for window changes.
win.off( "resize.bnLazySrc" );
win.off( "scroll.bnLazySrc" );
// Stop watching for document changes.
clearInterval( documentTimer );
}
// I start the render time if the window changes.
function windowChanged() {
if ( ! renderTimer ) {
startRenderTimer();
}
}
// Return the public API.
return({
addImage: addImage,
removeImage: removeImage
});
})();
// ------------------------------------------ //
// ------------------------------------------ //
// I represent a single lazy-load image.
function LazyImage( element ) {
// I am the interpolated LAZY SRC attribute of
// the image as reported by AngularJS.
var source = null;
// I determine if the image has already been
// rendered (ie, that it has been exposed to the
// viewport and the source had been loaded).
var isRendered = false;
// I am the cached height of the element. We are
// going to assume that the image doesn't change
// height over time.
var height = null;
// ---
// PUBLIC METHODS.
// ---
// I determine if the element is above the given
// fold of the page.
function isVisible( topFoldOffset, bottomFoldOffset ) {
// If the element is not visible because it
// is hidden, don't bother testing it.
if ( ! element.is( ":visible" ) ) {
return( false );
}
// If the height has not yet been calculated,
// the cache it for the duration of the page.
if ( height === null ) {
height = element.height();
}
// Update the dimensions of the element.
var top = element.offset().top;
var bottom = ( top + height );
// Return true if the element is:
// 1. The top offset is in view.
// 2. The bottom offset is in view.
// 3. The element is overlapping the viewport.
return(
(
( top <= bottomFoldOffset ) &&
( top >= topFoldOffset )
)
||
(
( bottom <= bottomFoldOffset ) &&
( bottom >= topFoldOffset )
)
||
(
( top <= topFoldOffset ) &&
( bottom >= bottomFoldOffset )
)
);
}
// I move the cached source into the live source.
function render() {
isRendered = true;
renderSource();
}
// I set the interpolated source value reported
// by the directive / AngularJS.
function setSource( newSource ) {
source = newSource;
if ( isRendered ) {
renderSource();
}
}
// ---
// PRIVATE METHODS.
// ---
// I load the lazy source value into the actual
// source value of the image element.
function renderSource() {
element[ 0 ].src = source;
}
// Return the public API.
return({
isVisible: isVisible,
render: render,
setSource: setSource
});
}
// ------------------------------------------ //
// ------------------------------------------ //
// I bind the UI events to the scope.
function link( $scope, element, attributes ) {
var lazyImage = new LazyImage( element );
// Start watching the image for changes in its
// visibility.
lazyLoader.addImage( lazyImage );
// Since the lazy-src will likely need some sort
// of string interpolation, we don't want to
attributes.$observe(
"bnLazySrc",
function( newSource ) {
lazyImage.setSource( newSource );
}
);
// When the scope is destroyed, we need to remove
// the image from the render queue.
$scope.$on(
"$destroy",
function() {
lazyLoader.removeImage( lazyImage );
}
);
}
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
}
);
</script>
</body>
</html>
@b-alidra
Copy link

@bennadel Thanks your directive, it still works great !

@MajdiJallouli
Copy link

Hello Ben, can i set a custom header when calling image src. i tried httpRequestInterceptor and no custom header was set up.
regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment