Create a “load more” widget using PHP, Ajax, Mootools, Bootstrap and Mustache.js

3 years ago David Walsh created an awesome (his words, but I agree!) NetTuts post called Create a Twitter-Like “Load More” Widget which is still today a reference on learning how to create “load more” widgets.

Today, with the emergence of projects such as Bootstrap (that took the web by a storm) and Mustache.js creating such a widget is getting easier.

Prerequisite

This tutorial requires several prerequisite in order to be completed successfully:

  • The server needs to run PHP5 to be able to use JSON functions.
  • The widget does not need any custom CSS we will only use the famous Bootstrap Framework.
  • We will use the lastest version of Mootools Core (1.4.5) and the Fx.Scroll Class from Mootools More.
  • To finish we will need to use the JavaScript version (Mustache.js) of the great Mustache project.

Step 1: PHP/MySQL

The PHP part is more or less what David Walsh created.

Start session

By using session the widget will remember how many posts you loaded during the last visit (in case you open a new page and come back).$_SESSION['posts_start'] is initialised and represent the number of posts already loaded in the page (by default two posts are directly loaded).

/* settings */
session_start();
$number_of_posts = 2; //2 at a time
$_SESSION['posts_start'] = $_SESSION['posts_start'] ? $_SESSION['posts_start'] : $number_of_posts;

Get posts from the Db

The function get_posts has two optional parameters ($start is increased every time you push the “more” button) to get only the posts you wish for.

/* grab stuff */
function get_posts($start = 0, $number_of_posts = 2) {
	/* connect to the db */
	$connection = mysql_connect('localhost','username','password');
	mysql_select_db('db_name',$connection);
	$posts = array();
	/* get the posts */
	$query = "SELECT post_title, post_content, post_name, post_date, ID FROM wp_posts WHERE post_status = 'publish' ORDER BY post_date DESC LIMIT $start,$number_of_posts";
	$result = mysql_query($query);
	while($row = mysql_fetch_assoc($result)) {
		// Remove html tag and truncate the content
		$row['post_content'] = substr(strip_tags($row['post_content']), 0,200) . '...';
		$posts[] = $row;
	}
	/* return the posts in the JSON format */
	return json_encode($posts);
}

Set up the JSON API

The purpose of the following piece of code is to return the desired posts in a JSON format for our widget convenience. If the $_GET['start'] variable is set in the requested URL then we know it has been requested from our widget(via AJAX request). It will then return posts and die.

You could also use the great JSON API plugin

/* loading of stuff */
if(isset($_GET['start'])) {
	/* spit out the posts within the desired range */
	$posts = get_posts($_GET['start'],$_GET['desiredPosts']);
	// decode the result to know the exact length of the result
	// It could be 2, 1 or 0 in this case
	$postsDecoded = json_decode($posts);
	// If the result is not empty we update the session
	if (!empty($postsDecoded))
	{
		/* save the user's "spot", so to speak */
		$_SESSION['posts_start']+= count($postsDecoded);
	}
	echo get_posts($_GET['start'],$_GET['desiredPosts']);
	/* kill the page */
	die();
}

Step 2: HTML (Bootstrap markups)

Our widget HTML is fairly simple. It contains a title with a posts counter (#widget .badge),a posts container (#widget .content) and a more button (#widget .more).

<div id="widget">
	<h4 id="widget-title">
		Widget Title <span class="badge badge-info"><?php echo $_SESSION['posts_start'] ?>
		</span>
	</h4>
	<div class="content" style="height: 300px; overflow: auto; margin: 0px;"></div>
	<button class="more btn btn-block">
		More <span class="caret"></span>
	</button>
</div>

Step 3: Mustache template (Bootstrap markups)

The mustache template contains the HTML for posts. The block {{#data}} YOUR HTML {{/data}} is rendered once for each item in the list ({data:[]} see below in the JavaScript part).

The inverted section opens with {{^data}} instead of {{#data}}. The block of an inverted section is rendered only if the value of that section’s tag is null, undefined, false, or an empty list.

For more details please check the Mustache.js documentation.

{{#data}}
<div class="item">
	<img class="thumbnail pull-left" width="50px" height="50px" src="http://placehold.it/50x50" style="margin: 0px 10px 5px 0px;">
	<a href="#">
		<h4 style="margin: 5px 0px;">{{post_title}}</h4>
	</a>
	<p>
		<small>{{post_content}}</small>
	</p>
	<div class="well well-small">
		{{post_date}}
	</div>
</div>
{{/data}}
{{^data}}
	<div class="alert alert-error">No data</div>
{{/data}}

Step 4: Mootools JavaScript

Initialize variables

  • The start variable represent the number of posts already loaded in the page.
  • The initialPosts variable is an array containing posts (either the default number of posts or the last number of posts saved in the session).
  • The desiredPosts variable is here 2 (See the PHP part above).
  • The template variable is either NULL or contains the Mustache template String.
var start = <?php echo $_SESSION['posts_start']; ?>;
var initialPosts = <?php echo get_posts(0,$_SESSION['posts_start']);  ?>;
var desiredPosts = <?php echo $number_of_posts; ?>;
var template = null;

DOM elements

// Widget element
var widget = $('widget'),
// Element to load the posts
content = widget.getElement('.content'),
// the more button
more = widget.getElement('.more'),
// the post counter
counter = widget.getElement('.badge');

Post Handler

The postHandler function purpose is to inject posts into our HTML.

The first execution loads and stores the Mustache template file (via a synchronous Ajax request) then uses the function Mustache.render(template, {'data' : data}) (that returns a HTML String of our posts) to populate the widget HTML.

Next executions (when clicking the “more” button) skip the Ajax request on the template and simply populate the widget HTML.

var postHandler = function(data){
	// Check if the template is already stored
	if (!template){
		// If not we get it
		new Request({
			url: 'load-more.mustache',
			method: 'get',
			async: false,
			onSuccess: function(responseText){
				// the response text is stored as the template
				template = responseText;
			},
			onFailure: function() {
				// insert the failure message
				widget.grab(alerts.templateFailure,'before');
				// get rid of the widget
				widget.dispose();
			}
			// Send the Ajax request
		}).send();
	}
	else{
		// Set the progress bar to 100%
		progressBar.setStyle('width', '100%');
		// Delay the normal more button to come back for a better effect
		more.set.delay(500, more, ['html','More <span class="caret"></span>']);
	}
	// Transform the template (String) into Elements that we can use
	var childrens =  new Element('div', {
		// Mustache requires an object property to reference in order to
		// create loops
		'html' : Mustache.render(template, {'data' : data})
	}).getChildren('*');
	// insert childrens at the end of the content element
	content.adopt(childrens);
	// Scroll to the first element loaded
	scroll.toElement(childrens[0]);
}

Ajax request to get the data

This part of code (I kept only the logic and removed effects) create a Ajax request instance for the more button to use when clicked. We ask the same page to return raw data (JSON) and on success event we either load this data into our template (if responseJSON.length > 0) or remove the more button (no more data to load).

// create the data Ajax request
var request = new Request.JSON({  
	url: 'load-more.php', // ajax script -- same page
	method: 'get',
	// Any calls made to start while the request is running will be ignored.
	link: 'ignore',
	// We do not want IE to cache the result
	noCache: true,
	onSuccess: function(responseJSON) {
		// Check if data is returned
		if (responseJSON.length > 0){
			// Update the total number of items
			start += responseJSON.length;
			// load items on the page
			postHandler(responseJSON);
		}
		else{
			// Remove the more button
			more.dispose.delay(500,more);
		}
	}
});

Add the click event to the “more” button

// add the click event to the more button
more.addEvent('click',function(){  
	// begin the ajax attempt
	request.send({
		data: {  
			'start': start,  
			'desiredPosts': desiredPosts  
		} 
	});  
});

Complete JavaScript

I extensively commented the following code to be as clear as possible. If you are having problems to understand just leave a comment, I will get back to you.

//domready event  
window.addEvent('domready',function() {
	// initialize variables
	var start = <?php echo $_SESSION['posts_start']; ?>;
	var initialPosts = <?php echo get_posts(0,$_SESSION['posts_start']);  ?>;
	var desiredPosts = <?php echo $number_of_posts; ?>;
	// either null or contains the mustache template
	var template = null;
	// Widget element
	var widget = $('widget'),
	// Element to load the posts
	content = widget.getElement('.content'),
	// the more button
	more = widget.getElement('.more'),
	// the post counter
	counter = widget.getElement('.badge');
	// Create alerts elements (Display Success or Failure)
	var alerts = {
			templateFailure : new Element('div',{'class' : 'alert alert-error','html' : 'Could not get the template.'}),
			requestEmpty : new Element('div',{'class' : 'alert alert-info','html' : 'No more data'}),
			requestFailure : new Element('div',{'class' : 'alert alert-error','html' : 'Could not get the data. Try again!'})
	}
	// create the Bootstrap progress bar element
	var progressElement = new Element('div', {
		'class': 'progress',
		'html': "<div class='bar'></div>",
		'styles': {
			'margin-bottom' : 0
		}
	});
	var progressBar = progressElement.getElement('.bar');
	// Create a scroll instance on the widget content
	// This Class is included in Mootools More
	var scroll = new Fx.Scroll(content, {
		duration: 1000,
		wait: false
	});
	// function that handle posts
	var postHandler = function(data){
		// Check if the template is already stored
		if (!template){
			// If not we get it
			new Request({
				url: 'load-more.mustache',
				method: 'get',
				async: false,
				onSuccess: function(responseText){
					// the response text is stored as the template
					template = responseText;
				},
				onFailure: function() {
					// insert the failure message
					widget.grab(alerts.templateFailure,'before');
					// get rid of the widget
					widget.dispose();
				}
				// Send the Ajax request
			}).send();
		}
		else{
			// Set the progress bar to 100%
			progressBar.setStyle('width', '100%');
			// Delay the normal more button to come back for a better effect
			more.set.delay(500, more, ['html','More <span class="caret"></span>']);
		}
		// Transform the template (String) into Elements that we can use
		var childrens =  new Element('div', {
			// Mustache requires an object property to reference in order to
			// create loops
			'html' : Mustache.render(template, {'data' : data})
		}).getChildren('*');
		// insert childrens at the end of the content element
		content.adopt(childrens);
		// Scroll to the first element loaded
		scroll.toElement(childrens[0]);
	}
	// place the initial posts in the page
	postHandler(initialPosts);

	// create the data Ajax request
	var request = new Request.JSON({  
		url: 'load-more.php', // ajax script -- same page
		method: 'get',
		// Any calls made to start while the request is running will be ignored.
		link: 'ignore',
		// We do not want IE to cache the result
		noCache: true,  
		onRequest: function() {
			// Set the progress bar to 0%
			progressBar.setStyle('width', '0%');
			// remove the more button innerHTML and insert the progress bar
			more.empty().grab(progressElement);
		},
		onSuccess: function(responseJSON) {
			// Check if data is returned
			if (responseJSON.length > 0){
				// Update the total number of items
				start += responseJSON.length;
				// Update the counter
				counter.set('html', start);
				// load items on the page
				postHandler(responseJSON);
			}
			else{
				// insert the empty message
				widget.grab(alerts.requestEmpty,'before');
				// Set the progress bar to 100%
				progressBar.setStyle('width', '100%');
				// Remove the more button
				more.dispose.delay(500,more);
				// remove the empty message after 4 seconds
				alerts.requestEmpty.dispose.delay(4000,alerts.requestEmpty);
			}
		},
		onFailure: function() {
			// insert the failure message
			widget.grab(alerts.requestFailure,'before');
			// Set the progress bar to 100%
			progressBar.setStyle('width', '100%');
			// Delay the normal more button to come back for a better effect
			more.set.delay(500, more, ['html','More <span class="caret"></span>']);
		}
	});
	// add the click event to the more button
	more.addEvent('click',function(){  
		// begin the ajax attempt
		request.send({
			data: {  
				'start': start,  
				'desiredPosts': desiredPosts  
			} 
		});  
	});
});