jQuery and Kohana, unobtrusive Ajax

Difficulty: Advanced

The tutorial you’ve all been waiting for judging from the keywords used to find this site ;) This post will cover the usage of jQuery and in particular Ajax together with Kohana. What we’ll do is make a list of students and then remove one of them using jQuery’s Ajax, so without refreshing the page! Yes, the whole Web 2.0 deal.

I do expect that you understand a little what Ajax is all about. It’s all about asynchronous requests. Normally, your browser asks a site for some data and the site kindly returns it and that’s it. With Ajax, the page your loading might make some extra requests later on. Such as, you press the delete button and without refreshing the page asks the server to delete the record through Ajax. jQuery comes in because it greatly eases the whole Ajax stuff as well as javascript in general. Thumbs up for the jQuery devs. Other js libraries can also be used of course but I have limited experience with them.

I’m gonna make this whole thing unobtrusive, this means that if you disable javascript it should still work.

This tutorial requires basic knowledge of ORM and the Template_Controller. Furthermore, if you use Kohana 2.1 you should download the request helper.

Let’s get started. Firstly, I use the table from the ORM tutorials and a similar model.
We create a students table with two students

--
-- Table structure for table `students`, dorm_id will not be used today 
--
 CREATE TABLE IF NOT EXISTS `students` (
  `id` mediumint(9) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) collate utf8_unicode_ci NOT NULL,
  `dorm_id` mediumint(9) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=4 ;
 
INSERT INTO `students` (`id`, `name`, `dorm_id`) VALUES
(1, 'student 1', 1),
(2, 'student 2', 1);

The matching model looks like:

//models/student.php
class Student_Model extends ORM{     
}

Can’t get easier than that, can it?
We’ll first create a list of students in a nice table. To do that we first need the template to work

//views/template.php
<html>
<head>
<title>Kohana jQuery Ajax tutorial</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
<? echo isset($head) ? $head : ''  ?>
</head>
<body>
<?= $content ?>
</body>
</html>

You can see I have a variable $head which is where all the jQuery code is going to be. Furthermore, Google is so nice to host the jQuery library for us.

Now, on to the actual Student_Controller and the associated view. Pay attention, the controller extends the Template_Controller for the layout magic.

//controllers/student.php
class Student_Controller extends Template_Controller {
	public function index()
	{
		$this->template->content=new View('student/index');
		$this->template->content->students=ORM::factory('student')->find_all();
		//$this->template->head=new View('student/ajax_delete');
	}
}

I load a view ’student/index’ and set the view variable $students with all available students from the database. The $this->template->head variable will be uncommented later on.
This view looks like

//views/student/index
<table id="students">
<?php
foreach ($students as $student)
{
echo "<tr><td>".$student->id."</td><td>".$student->name."</td><td>".html::anchor('student/delete/'.$student->id,'Delete',array('title'=>$student->id))."</td></tr>"; //The array in the third argument will give the hyperlink an extra attribute 'title' with as a value the student id, this will be used later on
}
?>
</table>

It loops over all students, outputs the id and the name together with a link to ’student/delete/$student_id’. Thus a link to another controller method. Clicking it now will give an error.

I’ll first add the delete method to the Student_Controller and setup the ajax deletion process.

public function delete($id)
{
	if(request::is_ajax())
	{
		$this->auto_render=false; //Disable the auto renderer, we don't want a layout in our ajax response
		$result=ORM::factory('student',(int) $id)->delete(); //delete the student 
                echo json_encode($result); //return a json encoded result
	}
	else
	{
		$this->template->content='nothing yet';
	}
}

So, when you now click on the delete link you’ll be sent to the delete function above and since your request is not ajax, you’ll see the inspiring message ‘nothing yet’.

Now we add the necessary jQuery stuff so the ajax call is being made. We uncomment the $this->template->head line in the index method above. The ‘ajax_delete’ view contains the following

//views/student/ajax_delete
<script type="text/javascript">
$(document).ready(function() 
{		
	$('#students .delete a').click(function() //select the 'Delete' link and catch the 'click'
	{
		var answer = confirm('Delete student'); //show confirmation
		if(answer==true) //if ok is pressed
		{
			var id = $(this).attr("title"); //get the student id from the title attribute of the link
		    $.ajax( //ajax request starting
			{
		       url: "student/delete/"+id, //send the ajax request to student/delete/$id
                       type:"POST",//request is a POSt request
		       dataType: "json",//expect json as return
		       success: function(result) //trigger this on success
			   {
				   if(true==result) //if deletion was successful
				   {
				   	  alert('Deleted'); //give confirmation
					   $('#students tr:has(td a[title ="'+id+'"])').fadeOut('slow'); //take away the deleted record from the table with a nice fade out
				   }
			   }
		    });
		 }
  		return false //return false to prevent the link from working
	}); 
});
</script>

If all is well you, go the the ’student’ page and click on the delete button. You’ll see a confirmation screen which asks whether to delete. Press ok to delete and you’ll get an alert and the record will fade out. Neat, isn’t it.
Student list with deletion confirmation
Now for the unobtrusive part. What happens if you have js disabled? Right now you’ll get the ‘nothing yet’ message.

Delete requests are something you shouldn’t handle with GET but with POST (or DELETE) so we need a little form.There are a dozen ways to handle this but the following is simple, change the delete method to this.

//controllers/student.php
public function delete($id)
{
	if(request::is_ajax())
	{
		$this->auto_render=false; //Disable the auto renderer, we don't want a layout in our ajax response
		$result=ORM::factory('student',(int) $id)->delete(); //delete the student 
                echo json_encode($result); //return a json encoded result
	}
	else
	{
                $student=ORM::factory('student',(int) $id);
		$form=new Forge('', 'Remove Student:'.$student->name, 'POST');
		$form->hidden('id')->value((int)$id);
		$form->submit('submit');
 
		if($form->validate() AND !empty($_POST))
		{
			$student->delete();
			url::redirect('student');
		}
		else
		{
			$this->template->content=$form;
		}
	}
}

Now if you go to ’student/index’ and press the ‘delete’ button and your javascript is off you’ll be directed to this form where you can confirm the deletion.

Some words of advice. If you do Ajax and javascript, Firebug and the Webdeveloper Toolbar are excellent tools for debugging in Firefox. Firebug shows you for example any Ajax requests and the response of them whilst the Webdeveloper Toolbar will allow you to disable javascripts in two clicks or so. Of course this also applies to similar tools for other browsers.

That’s it. There are a couple of improvements that can still be made but I’ll leave it at this. I hope it’s clear and useful. I’ll happily respond to any questions. It’s quite a lengthy tutorial so I’ll stop now.


13 Responses to “jQuery and Kohana, unobtrusive Ajax”

  1. ghaez Says:

    Nice tutorial. Thanks!

  2. Sam Clark Says:

    Nice work dlib, I was planning something similar but you’ve got it perfectly covered here. Kudos for doing it Unobtrusively too, we need more frameworks adopting that approach.

  3. Edy Says:

    Thank you, dlib. Very nice tutorial.

  4. Matt Says:

    Awesome. Well done.

  5. Klaas Says:

    very informative!

  6. bennythemink Says:

    really clear tutorial, good work and thanks!

  7. neovive Says:

    I agree with all of the above comments. This is a great tutorial that combines a lot of useful concepts.

  8. LoneWolf367 Says:

    Good tutorial. This is my first time using jQuery. I’ve applied and modified your example for some links on my application.

    How would one go about making an entire div disappear?

    I’ve tried (and I have the title attribute set): $(’div:has(div[title="'+id+'"])’)

  9. dlib Says:

    Are you sure your selection is right? You can change visibility by for example adding a class.

    $(’div:has(div[title="'+id+'"])).hide(); //sets class to visible
  10. Dave Says:

    Great tutorial (especially the ORM stuff), but why not give the links an ID attribute of the ID, instead of a title


    html::anchor('student/delete/'.$student->id,'Delete',array('id'=>$student->id))

    (I bet the HTML tags wont work, but you should get the idea)

    Another way to do unobtrusive Ajax with JQuery:


    $('#students .delete a').click(function() {
    $(this).load($(this).val("href")+'?ajax=true'))
    }

    Then have the controller/view either showing the form or returning HTML with a script to do the fade out.

    That (kind of) keeps more logic out of the JS.

  11. cevarief Says:

    Tahnks for the tutorial.

    Btw, do you have this kind of tutorial but using mootools ?

  12. dlib Says:

    I have zero experience with mootools but I expect the difference to just be syntactical.

  13. toulon Says:

    I must be missing something easy here, but I can’t figure it out. As I worked through your example everything was going fine until I tried to use ajax. It appears that I am missing some really basic components. See details below

    Snippet of code from Student_Controller

    public function delete($id)
    {
    if (request::accepts_xhtml()) {
    echo “Accepts xhtml”;
    } else {
    echo “Does not accept xhtml”;
    }
    if(request::is_ajax())
    {
    $this->auto_render=false; //Disable the auto renderer, we don’t want a layout in our ajax response
    $result=ORM::factory(’student’,(int) $id)->delete(); //delete the student
    echo json_encode($result); //return a json encoded result
    }
    else
    {
    $this->template->content=’nothing yet’;
    }
    }
    My display keeps saying ‘nothing yet’. As I dug into the code I found that my browsers (firefox, chrome, IE) on both Windows and Linux display the same message
    This is the output from http://mfnlamp2/index.php/student/overview
    Does not accept xhtml
    In is_ajax
    Array
    (
    [HTTP_HOST] => mfnlamp2
    [HTTP_USER_AGENT] => Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008070206 Firefox/3.0.1 FirePHP/0.1.1
    [HTTP_ACCEPT] => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    [HTTP_ACCEPT_LANGUAGE] => en-us,en;q=0.5
    [HTTP_ACCEPT_ENCODING] => gzip,deflate
    [HTTP_ACCEPT_CHARSET] => ISO-8859-1,utf-8;q=0.7,*;q=0.7
    [HTTP_KEEP_ALIVE] => 300
    [HTTP_CONNECTION] => keep-alive
    [HTTP_REFERER] => http://mfnlamp2/index.php/student/
    [HTTP_COOKIE] => has_js=1
    [PATH] => /sbin:/usr/sbin:/bin:/usr/bin
    [SERVER_SIGNATURE] => Apache/2.2.3 (Red Hat) Server at mfnlamp2 Port 80

    [SERVER_SOFTWARE] => Apache/2.2.3 (Red Hat)
    [SERVER_NAME] => mfnlamp2
    [SERVER_ADDR] => 172.16.22.176
    [SERVER_PORT] => 80
    [REMOTE_ADDR] => 172.16.22.176
    [DOCUMENT_ROOT] => /export/home/wwwroot
    [SERVER_ADMIN] => root@localhost
    [SCRIPT_FILENAME] => /export/home/wwwroot/index.php
    [REMOTE_PORT] => 35426
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_PROTOCOL] => HTTP/1.1
    [REQUEST_METHOD] => GET
    [QUERY_STRING] =>
    [REQUEST_URI] => /index.php/student/delete/1
    [SCRIPT_NAME] => /index.php
    [PATH_INFO] => /student/delete/1
    [PATH_TRANSLATED] => /export/home/wwwroot/student/delete/1
    [PHP_SELF] => /index.php/student/delete/1
    [REQUEST_TIME] => 1220976087
    )
    I see no reference to HTTP_X_REQUESTED_WITH
    The check to see if the request made is AJAX fails
    I know the javascript code is being loaded, I can see it using inspect in firebug
    Complete code located at http://pastebin.com/m6184b5b6

Leave a Comment