Ajax templating within Joomla

First of all this post isn't really anything to do with Fabrik, but I'm going to expand the blog to talk about various Joomla 1.5 coding problems, solutions and best practices. Please feel free to correct, contradict and eventually even concur with what I'm saying in the comments!

These posts won't be about how to start coding Joomla components, there's lots of tutorials and documentation out there to get you started.

So this first post will address an issue that I have constantly struggled with and until today have never had a solution that I felt was totally acceptable.

Imagine, if you will, that we have a component with a list of items and that you want to create some neat little Ajax code to scroll through that list.

We also want the code to degrade nicely so that if the user doesn't have JavaScript enabled they can still scroll the list. Finally, and here is the part that took me a while to find what I consider an elegant solution we want to ensure that there is no duplication of code used to create the updated list items. In other words there is only one place in which the list's html is generated, regardless of whether we are calling the code via Ajax or not.

The component name that I'm basing this demo on is called 'Fabble' obviously where ever 'fabble' appears you would need to replace that with your own component name. Within the 'Fabble' component we're going to be displaying a list of groups

To ensure the degradation we will build the code in the usual fashion.

The Controller

First of all we'll need to create a controller class in components/com_fabble/controllers/groups.php

defined('_JEXEC') or die();
jimport( 'joomla.application.component.controller' );
class FabbleControllerGroups extends JController{
function display(){
$document =& JFactory::getDocument();
$viewName = JRequest::getVar( 'view', 'home', 'default', 'cmd' );
$modelName = $viewName;
$viewType = $document->getType();
// Set the default view name from the Request
$view = &$this->getView( $viewName, $viewType );
// Push a model into the view
$model = &$this->getModel( $modelName );
if (!JError::isError( $model ) && is_object( $model)) {
$view->setModel( $model, true );
}
// Display the view
$view->assign( 'error', $this->getError() );
return $view->display();
}
} ?>

This is pretty standard code for a controller class, where it sets up the model, creates the view and assigns the model to the view. I've come to the conclusion that its clearer to create a controller per section of your site rather than making on large controller for the entire component.

The view

Next we will create an empty view class in components/com_fabble/views/groups.html.php and get a reference to the document:

// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die();
jimport('joomla.application.component.view');
class fabbleViewGroups extends JView {
function display($tpl = null) {
$document =& JFactory::getDocument();
}
} ?>

Now, inside the display method, we're going to get some variables from the group model:

$nav     =& $this->get( 'nav' );
$groups =& $this->get( 'Data' );

The view's get function maps to the corresponding groups model we assigned in the controller. The model file should be located in

components/com_fabble/models/groups.php and will look like this:

class FabbleModelGroups extends JModel {

/**@var array group objects */
var $groups = null;

/** @var int default num of groups to show per page */
var $_navLimit = 5;

/**
* get a list of groups
* * @return array group objects
*/

function getData()
{
if (!isset( $this->groups )) {
$query = $this->_buildQuery();
$limit = JRequest::getVar( 'limit', $this->_navLimit, '', 'int' );
$limitstart = JRequest::getVar( 'limitstart', 0, '', 'int' );
$this->groups = $this->_getList( $query, $limitstart, $limit );
}
return $this->groups;
}

/**
* build the query to get the list of groups
*
* @return string query
*/

function _buildQuery() {
return "SELECT *, g.id AS id, (SELECT COUNT(id) FROM #__fabble_person_groups\n".
" AS pg where pg.group_id = g.id) AS member_count, p.displayName as created_by_name\n".
" FROM #__fabble_groups AS g LEFT JOIN #__fabble_people AS p ON p.user_id = g.created_by ";
}

/**
* get the navigation for the group list
*
* @return object navigation
*/

function getNav() {
jimport('joomla.html.pagination');
$db =& JFactory::getDBO();
$db->setQuery( "SELECT count(*) FROM #__fabble_groups" );
$limit = JRequest::getVar( 'limit', $this->_navLimit, '', 'int' );
$limitstart = JRequest::getVar( 'limitstart', 0, '', 'int' );
$total = $db->loadResult();
return new JPagination( $total, $limitstart, $limit );
}
}

So, when the view calls $this->get( 'nav' ); it is in fact calling the models getNav() function, likewise the $this->get( 'Data' ) calls the models getData() function.

Now, back in the view, lets assign those variables to the view so they can be used within our template. As they are both objects we need to use the 'assignRef' method:

$this->assignRef( 'groups', $groups );
$this->assignRef( 'nav', $nav );

We also need to include the JavaScript classes we want to use:

    JHTML::script('groups.js', 'media/com_fabble/js/');
JHTML::script('ajaxnav.js', 'media/com_fabble/js/');

Then create an instance of our JavaScript class that is going to observe the navigation controls and manage the Ajax call to the server when the user interacts with those controls:

     $opts            = new stdClass();
$opts->nav = $nav->getProperties();
$opts->livesite = JURI::base();
$opts->page = JURI::current() . "?" . $_SERVER['QUERY_STRING'];
$opts = json_encode($opts);
$script = "window.addEvent('domready', function(){".
" new FabbleGroups($opts, {});". "});";
$document->addScriptDeclaration($script);

I'm presuming you have a recent version of PHP5 here that includes the json_encode function. Its best to make your json object in this fashion rather

than creating it by hand as it's less error prone. The last three lines add a script to the document's head which waits for the DOM to be ready before

creating an instance of our FabbleGroup JavaScript class (that's going to be in the file media/com_fabble/js/groups.js)

Finally we want to display our template:

parent::display($tpl);

So, just to recap, here's the full view:

// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die();
jimport('joomla.application.component.view');
class fabbleViewGroups extends JView {
function display($tpl = null) {
$document =& JFactory::getDocument();
$nav =& $this->get( 'nav' );
$groups =& $this->get( 'Data' );
$this->assignRef( 'groups', $groups );
$this->assignRef( 'nav', $nav );
JHTML::script('groups.js', 'media/com_fabble/js/');
JHTML::script('ajaxnav.js', 'media/com_fabble/js/');
$opts = new stdClass();
$opts->nav = $nav->getProperties();
$opts->livesite = JURI::base();
$opts->page = JURI::current() . "?" . $_SERVER['QUERY_STRING'];
$opts = json_encode($opts);
$script = "window.addEvent('domready', function(){".
" new FabbleGroups($opts, {});".
"});";
$document->addScriptDeclaration($script);
parent::display($tpl); }
}
?>

 

The template code

Now lets move on to the template itself. I'm going to create two template files, you will see why I don't merge them into one file later on. The files are

components/com_fabble/views/group/tmpl/default.php
components/com_fabble/views/group/tmpl/default_items.php

Here's default.php's content:

<h1><?php echo JText::_('Groups') ?></h1>
<div id=”fabble-groups”>
<?php
$user =& JFactory::getUser();
if ($this->userid != 0) { ?>
<a class=”fabble-add” href=”index.php?option=com_fabrik&view=form&fabrik=<?php echo $this->groupFormid ?>”>
<?php echo JText::_(’Add group’); ?>
</a>
<?php }
?>
<ul class=”fabble-list”>
<?php echo $this->loadTemplate(’items’) ?>
</ul>
<div class=”fabble-nav”>
<?php print_r( $this->nav->getPagesLinks())?>
</div>
</div>)?>
Pretty straight forward right? The only interesting bit here is where we output the html generated from 'default_items' template with the code:
 <?php echo $this->loadTemplate(’items’) ?>
Now onto the meat of the template found in default_items.php
<?phpforeach($this->groups as $group){
?>
<li>
<h2><a href=”index.php?option=com_fabble&controller=group&id=<?php echo $group->id ?>”><?php echo $group->label ?></a></h2>

<div>
<p><?php echo JText::_(’Owner’) ?>: <?php echo $group->created_by_name ?></p>
<img src=”media/com_fabble/images/group.png” alt=”<?php echo JText::_(’Members’) ?>” />
<?php echo $group->member_count . ” ” . JText::_(’Members’);?>
</div>
</li>
<?php }?>

This will iterate over our group list and output a series of items.

At this point if you went to index.php?option=com_fabble&controller=groups you would see a list of groups which you could navigate through, but there would be no Ajax navigation. As our JavaScript will run on top of this, in a manner which is called progressive enhancement, and thus meets our initial requirement that the page should degrade correctly with JavaScript turned off.

 

A sprinkling of JavaScript magic

Now to add the JavaScript to control the page, we'll start by looking at media/com_fabble/js/groups.js

var FabbleGroups = new Class({
getOptions: function(){
return {
'nav':{},
'livesite':'',
'page':''
};
},
initialize: function(options){
this.setOptions(this.getOptions(), options);
this.showGroups = new Ajax('index.php?option=com_fabble&controller=groups&format=raw',
{
'data':{
},
'onComplete':function(d){
$('fabble-groups').getElement('ul').setHTML(d);
this.nav.updateNav();
}.bind(this)
});
this.nav = new AjaxNav('fabble-groups', {
'ajax':this.showGroups,
'nav':this.options.nav,
'page':this.options.page,
'livesite':this.options.livesite
});
} });
FabbleGroups.implement(new Events);
FabbleGroups.implement(new Options);

The initialize() function is called automatically when an instance of the class is created, which we have already done from within the view. The setOptions call will assign the json object we passed in from the view to the class instance's 'this.options' variable.

Next we create our Ajax object and assign it the this.showGroups variable. When triggered it will call the url 'index.php?option=com_fabble&controller=groups&format=raw' and once the Ajax request has completed it will run the 'onComplete' function.

Finally we create an instance of the navigation class. This is separate from the FabbleGroup class so that you can reuse it in other places within your component.

So, with no further ado, lets take a look at how it looks (it should be placed in /media/com_fabble/js/ajaxnav.js

/** *    observes the standard Joomla pagenavigation
and replaces its events with ajax loading events */
var AjaxNav = new Class({
getOptions: function(){
return {
'livesite':'', //live site url
'nav':{}, // the ajax function to run when the navigation is run
'total':0, //total number of records for the nav
'page':''
};
},

initialize: function(element, options){
this.setOptions(this.getOptions(), options);
this.element = $(element);
this._addSpinner();
this.watchNav();
},

_addSpinner: function() {
if(!this._getNav()){
return false;
}
if($type(this.navspinner) !== false){
return;
}
this.navspinner = new Element('img', {'styles':{'display':'none'}, 'src':this.options.livesite+'/media/com_fabble/images/ajax-loader.gif'});
new Element('li').adopt(this.navspinner).injectInside(this._getNav());
},

_getNav: function() {
return this.element.getElement('.pagination');
},

watchNav: function() {
var nav = this._getNav();
var jnav = this.options.nav;
if(!this._getNav()){
return;
}
var total = nav.getElements('li').length;
nav.getElements('li').each(function(li, x){
li.addEvent('click', function(e){
new Event(e).stop();
this._addSpinner();
this.navspinner.setStyle('display','inline');
if(li.getElement('a')){
switch(x){
case 1: //first ??
this.options.jnav.limitstart = 0
break;
case 2: //second??
jnav.limitstart = jnav.limitstart - jnav.limit;
break;
case total -4: //next
jnav.limitstart = jnav.limitstart + jnav.limit;
break;
case total - 3: //end
jnav.limitstart = jnav.total - jnav.total % jnav.limit;
break;
case total - 1: //spinner
return;
break;
default:
jnav.limitstart = ((x - 3) * jnav.limit);
break;
}
this.options.ajax.options.data.limitstart = jnav.limitstart;
this.options.ajax.request();
}
}.bind(this))
}.bind(this));
},
updateNav: function(){
var nav = this._getNav();
var jnav = this.options.nav;
this.navspinner.setStyle('display','none');
var total = nav.getElements('li').length;
nav.getElements('li').each(function(li, x){
switch(x){
case 0:
case total -1:
case total -2:
//ignore
break;
case 1:
case 2:
if(jnav.limitstart != 0){
if(!li.getElement('a')){
var span = li.getElement('span');
var title = span.getText();
li.empty();
if(x == 1){ s = 0;}else{ s = jnav.limitstart - jnav.limit;}
new Element('strong').adopt(
new Element('a', {'title':title, 'href':this.options.page + '&start='+s}).setText(title)).injectInside(li);
}
}else{

if(li.getElement('a')){
var title = li.getElement('a').getText();
li.empty();
new Element('span').setText(title).injectInside(li);
}

}
break;
case total -4:
case total -3:
if(jnav.limitstart != jnav.total - jnav.total % jnav.limit){
if(!li.getElement('a')){
var span = li.getElement('span');
var title = span.getText();
li.empty();
if(x == 1){ s = 0;}else{ s = jnav.limitstart - jnav.limit;}
new Element('strong').adopt(
new Element('a', {'title':title, 'href':this.options.page + '&start='+s}).setText(title)).injectInside(li);
} }else{
if(li.getElement('a')){
var title = li.getElement('a').getText();
li.empty();
new Element('span').setText(title).injectInside(li);
}
}
break;
default:
if(jnav.limitstart == (x-3) * jnav.limit){
if(li.getElement('a')){
var title = li.getElement('a').getText();
li.empty();
new Element('span').setText(title).injectInside(li);
}
}else{
if(!li.getElement('a')){
var span = li.getElement('span');
var title = span.getText();
li.empty();
if(x == 1){ s = 0;}else{ s = jnav.limitstart - jnav.limit;}
new Element('strong').adopt(
new Element('a', {'title':title, 'href':this.options.page + '&start='+s}).setText(title)).injectInside(li);
}
}
break;
}
}.bind(this))
} });
AjaxNav.implement(new Events);
AjaxNav.implement(new Options);

Wow, slighlty longer this class isn't it?

Lets discet the watchNav function, whose purpose is to override the standard navigation events with out own:

var nav = this._getNav();

Will get a refernence to the standard Joomla navigation list controls we added in the template.

 nav.getElements(’li’).each(function(li, x){ …. }

will get every <li> inside the navigation and iterate over each of those li's

 

li.addEvent('click', function(e){

Assigns a new event to each of the li's.

new Event(e).stop();

this will stop the event from bubbling, which in turn means that the navigation's standard code will not be run.
The following switch statement, whilst long winded, simply works out which button has been pressed and updates the jnav objects limistart value

We then update the ajax object's limitstart value we passed in from the FabbleGroup class to the new limitstart value

this.options.ajax.options.data.limitstart = jnav.limitstart;

and finally fire off the request

this.options.ajax.request();

A raw view

Remember that in the FabbleGroup Javascript class we had set the Ajax's object's url to:
'index.php?option=com_fabble&controller=groups&format=raw'

This will tell Joomla to render a raw view of the page we are currently on.
So we need to create a new file: components/com_fabble/views/groups/view.raw.php - note the 'raw' in the file name, this is the view that Joomla looks for as we passed in '&format=raw' in the Ajax request's querystring.

Here's the content of the file:

/**  * @package Joomla
* @subpackage Fabble
* @copyright Copyright (C) 2005 Rob Clayburn. All rights reserved.
* @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.php */
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die();
jimport('joomla.application.component.view');
class fabbleViewGroups extends JView {
function display($tpl = null) {
// Initialize variables
$this->assignRef( 'groups', $this->get( 'Data' ) );

parent::display(’items’);
}
} ?>

So again, we are getting the list of groups. As we have updated the limistart varaible when sending the Ajax request the data returned from this function will reflect the current page you have navigated to.

Then, instead of calling the main template ('default.php') we call 'default_items.php' with the function:

parent::display('items');

This has the effect of returning to the Ajax object's onComplete function only the updated <li>'s, precisely the content we are after.

The last step in updating the list is in the FabbleGroup Javascript class's this.shoWGroups onComplete function:

 

'onComplete':function(d){
$('fabble-groups').getElement('ul').setHTML(d);
this.nav.updateNav();
}.bind(this)

So we update the <li>'s html with that returned from the raw view and then tell the navigation object to update itself, basically this invloves iterating over all of the navigation lists items and setting or removing their links.

 

Other methods I considered

Sending JSON

I have to say that I didn't start out doing things in this manner. My first idea was to make the ajax call get a json object directly from the group controller, once the json data received the FabbleGroup JavaScript class would build the required elements and insert them into the list.

I liked this to start off with because the data passed back to the page was smaller in size. However, as soon as the content inside the list becomes complex its was quiet hard to write the javascript to create it, and if the template changed then I would need to update the javascript code to reflect those changes, so all in all a very brittle solution.

Javascript Agogo

So I knew that I had to have the html generated in one place, my first thought was that I could do it all within the javascript classes. This would involve passing pure json to the scripts which would then build the pages. Somewhere my love of JavaScript made me really consider this for a moment, then I realised that it would mean my users would have to have Javascript enabled and it would make adjusting the templates harder.

So to summarise I'm pretty darned happy with the soluton I have now. It degrades beautifully whilst still being flexible and robust.

Read more...


Fabrik 2.0b3 released

We are proud to release the next and hopefully last beta version of Fabrik, Fabrik 2.0b3. This release contains a large number of bug fixes (over 470 commits since the second beta) from the previous 2.0b2 release from June.

This release would not have been possible without the communities help in reporting bugs, suggesting fixes, posting translations and general inspiration! I encourage you all to continue helping us so we can quickly move towards release candidates and then a final version

Installation instructions

Clean installation

Download com_fabrik2.0b3.zip from the downloads site. Log into your site's administration section and select Extensions->Install/Uninstall. In the 'Upload Package File' section press the 'browse' button to locate the downloaded zip file. Once selected press 'Upload file and Install'. Fabrik is a large package and the installation may time out with this method. An alternative way is to create a folder called 'com_fabrik2.0b3' on your server in its tmp directory (e.g. /location/of/mysite/tmp/com_fabrik2.0b3) and upload the files contained within com_fabrik2.0b3.zip to this folder. Once uploaded, naviagate to Joomla's installation page as before, but this time enter the path to the uploaded files in the 'Install from Directory' section (e.g. /location/of/mysite/tmp/com_fabrik2.0b3) and press the 'Install' button.

Upgrading

First of all make a back up of all your current Fabrik files and database tables, if anything goes wrong in the upgrade process you will always be able to revert to your old files.

Now install Fabrik as if it was a new component. Your data won't be overwritten but Fabrik's files will be updated.

There are no database changes to be made.

You may have to re-edit and save date and user elements. Please note that date formatting now uses php's strftime formatting method.

Release Notes

  • Faster, stronger, better ! There has been a concerted effort to reduce the number of queries and memory usage. Queries have been cut down by up to 400% in certain places.
  • GEORSS output in RSS feeds
  • JACL+ integration - use groups created by JACL+ to increase the granularity of access control within Fabrik.
  • Experimental option to combine and compress all your fabrik javascript files into single files.

Tables

  • Improved administration delete table options - thanks to Cyber Fabrik
  • Copy row table plug-in - This simple plug-in allows your users to select and duplicate records.
  • Option to have a single search field that will search all of the table's data.

Forms

  • New group setting allowing their elements to be rendered in columns
  • New 'labels above' form template
  • Create simple interactions on your forms without knowing Javascript, e.g. fade out this group when this elements value is true.
  • The regex validation from Fabrik 1.0.x has been ported over, also allowing for the replacement of posted data with specified regex/text.

Visualization plug-ins

  • Most plug-ins have been refractored to use the MVC layout
  • Clustering options in the google Map visualization
  • Calendar - events can now have start and end dates (thx Barbara!)

CSV Export

  • Choose whether to apply the table's filters to the CSV data
  • In addition choose which fields to export

Download the full SVN log here

Download Fabrik 2.0b3

Read more...


Protonotes annotation plugin released

Need a quick way to annotate your Joomla and Fabrik pages, during the development phase? If so then look no further, I've just released a small Joomla 1.5 plug-in that allows you to use the Protonotes service to annotate your web pages. Notes can be dragged, hidden, marked as reviewed and completed. To get started, dowload the protonotes plug-in from Joomlacode and create an account on the protonotes site.

Read more...


See Fabrik @ linux exop, London Olympia

I will be exhibiting Fabrik at London Linux Expo Live from Thursday the 23rd October to Saturday the 25th. Fabrik will be in the .ORG Village, sharing a stand with Joomla tools, right next door to the Joomla stand, so if you can please come along for a chat I know that its rather short notice, I only got the go ahead for this yesterday and have since been running around trying to get flyers. business cards and t-shirts printed I hope to see some of you there!

Read more...


igfest: The moosehunt is on!

If you are around Bristol (UK) this weekend, then you could be taking part in Moosehunt at the igfest- thanks in part to Fabrik! Huh? Moose, Bristol, surely not! Have I finally lost my mind? Not at all! Moosehunt is a location based mobile game designed by Simon Evans for Bristol's igfest:
"Moose will be making his autumn migration from the deepest Forest of Dean to Bristol over the week before the festival. Your mission is to find him and photograph him. Text 'moose' to 60300 to receive a map of the moose's current location. But be careful, the moose will be told your location and if he 'shoots' you before you 'shoot' him you are out of the game. Prizes for photographs in various categories including first sighting."
Simon asked me to collect the SMS messages and MMS file uploads and plot them onto a Google map, something for which Fabrik was ideally suited. Its been great fun working on such a off-beat project. Check out the spoof documentary video Simon produced for the game, if only to see the absurd (or perhaps slightly perturbing) costume Simon will be wearing all this weekend.

Read more...


Fabrik is not affiliated with or endorsed by the Joomla! Project or Open Source Matters. The Joomla! logo is used under a limited license granted by Open Source Matters the trademark holder in the United States and other countries.