How to rig symfony-project with jQuery for AJAX

Introduction

This article covers how to use the jQuery library in to power ajax applications developed in the PHP5 framework, Symfony-project. You need to have basic understanding of AJAX, JavaScript and the MVC structure of Symfony-project to get the fullest out of this article.

The example shown below uses only a few lines of JavaScript to globally power the typical ajax calls used on the site Kevo.com. It is also totally degradable and easy to remove and test your application without AJAX enabled.

Typical Use of AJAX

symfony projects comes out of the box with the prototype javascript library (same as ruby on rails) and the typical helpers such as return link_to_remote('interested?', array( 'url' => 'user/interested?id='.$question->getId(), 'update' => array('success' => 'block_'.$question->getId()), 'loading' => "Element.show('indicator')", 'complete' => "Element.hide('indicator');".visual_effect('highlight', 'mark_'.$question->getId()), )); which would then output <a href="#" onclick="new Ajax.Updater({success:'block_264'}, '/user/interested/id/264', {asynchronous:true, evalScripts:false, onComplete:function(request, json){Element.hide('indicator');new Effect.Pulsate('mark_264', {});}, onLoading:function(request, json){Element.show('indicator')}}); return false;">interested?</a> but we should all know we dont use inline javascript for the same reason we don't use inline CSS, which is becasue it directly dictates the behaviour in a markup language which should only give structure to the document. the real advantage, as with CSS is flexibility and maintenance. JavaScript should be used to enhance the UI, not define it and it should also degrade. This is why we're going to replace all of these inline calls with a simple, global, few lines of javaScript.

the jQuery way

what if we could just define a which links we want to have ajax actions by adding the class "ajax" to them. Then, using jQuery's DOM selectors to grab the link with class "ajax" and process it returning the result. <div id="profile_add_favorite"> <a class="ajax" href="site.com/favoriteAdd?target=profile_add_favorite">do ajax</a> </div> function parseQuery ( query ) { var Params = new Object (); if ( ! query ) return Params; // return empty object var Pairs = query.split(/[;&]/); for ( var i = 0; i < Pairs.length; i++ ) { var KeyVal = Pairs[i].split('='); if ( ! KeyVal || KeyVal.length != 2 ) continue; var key = unescape( KeyVal[0] ); var val = unescape( KeyVal[1] ); val = val.replace(/\+/g, ' '); Params[key] = val; } return Params; } $("a.ajax").click(function() { var url = this.href; var action = url.split("?"); var queryString = url.replace(/^[^\?]+\??/,''); params = parseQuery( queryString ); $('#' + params.target).prepend('<img src="/images/loading.gif" />'); $('#' + params.target).load(action[0] + '/ajax', function(){ $("#" + params.target).highlightFade('#ADDCF0', 1000, null, 'exponential'); }); return false; });

as you can see the target of this function is defined in the HTML, but only accessed by javascript. it tells the ajax action where to go.

the line $('#' + params.target).load(action[0] + '/ajax', function(){ tells the ajax location WHERE to go, and WHICH action to use in the controller.

and the ACTION code

public function executeFavoritesAdd() { $this->favoritesAdd(); return $this->redirect("@profile_home?profile_title={$title}&profile_id={$id}"); } public function executeFavoritesAddAjax() { $this->favoritesAdd(); } private function favoritesAdd() { //save favorite here do stuff and return result into a partial. }

now you can remove prototype and scriptalicious from symfony-project, saving you quite a bit of load space. You will also have removed inline javascript and made a global ajax action that can work on any link using the same code.

but this isn't complete yet, I'll show you how we need to do the routing and partials

<div id="profile_add_favorite"> <?php echo include_partial('addFavorite', array('profile' => $profile)) ?> </div> //and the contents of _addFavorite.php <? if ($sf_user->isFavorite($profile->getId())): ?> <?= link_to("{$profile->getTitle()} is in My Favorites", '@user_favorites');?> <? else: ?> <?= LinkLoginRequired("Add {$profile->getTitle()} to Favorites <img src=\"/images/icon/fav.jpg\" />" , "@user_favorites_add?title={$profile->getURLTitle()}&id={$profile->getID()}", false, array('class' => 'ajax', 'query_string' => 'target=profile_add_favorite') ); ?> <? endif; ?>

the _addFavorite.php partial says "if this profile is in your favorites, link to your favorites, otherwise show the link to add them"

//add this to your routing profile_add: url: /profile/add param: { module: profile, action: profileAdd } profile_add_ajax: url: /profile/add/ajax param: { module: profile, action: profileAddAjax }

And be sure to create the template "profileAddAjaxSuccess.php" which includes only the partial. This is what the ajax action calls, which then calls the partial executes it and inserts the result into the target location

//contents of "profileAddAjaxSuccess.php" <?php echo include_partial('addFavorite', array('profile' => $profile)) ?> //that's it

test it out. You'll notice that the entire site is now imput into that div! , so turn off the layout on your action

profileAddAjaxSuccess: has_layout: off

Cool. This works perfectly fine right now, but what if I told you we could turn up the heat a bit more and really make this entire ajax thing shine. Lets add in the ability to pass multiple peices of information back to the browser at one time using JSON, which also parses faster than its XML equivalent.

JSON is just a fancy name for a javascript object literal, like this obj = {}. or like this obj = {one: this, two: that}. Now instead of the variable names one, two, lets pass html & message , Something like this

<?php $html = KevoUtilities::escapeJavascript(get_partial('addFriend', array('viewUser' => $targetUUser ))); ?> <?= "{ html : '{$html}', msg : '{$msg}'}"; ?>

So instead of parsing the partial and echoing out the result which is transported w/ XML and inserted into the location, we parse the Partial with the get_parial(); function (escape line breaks to make it a single liner with our KEVO utilities) and return , literally { html : '

this is my template!
', msg: 'success!' }

Now lets parse this with a modified ajax function and stick things where they need to go

$("a.ajax").click(function() { var url = this.href; var action = url.split("?"); var queryString = url.replace(/^[^\?]+\??/,''); var params = parseQuery( queryString ); $("#" + params.target).fadeTo('slow', .01); $.post(action[0] + '/ajax' + (action[1] ? '?' + action[1] : ''), '', function(json) { eval("var args = " + json); $("#" + params.target).html(args.html); $("#" + params.target).fadeTo('slow', 100); $("#ajaxmessage").html(args.msg); $("#ajaxmessage").fadeIn('slow', function(){ setTimeout(function(){ $('#ajaxmessage').fadeOut('slow'); }, 2000); }); }); return false; });

I've also added a div that is absolutely positioned with the ID "ajaxmessage" to be the container for the message results , since we're passwing the message along w/ the HTML contents.

Next Steps

Often in symfony you'll end up using some routing and referrer tricks and components. These provide challenges to AJAX components because we want to create easy ways to store referrers and posted values so those can be forwarded or redirected wherever necessary.

An example would be a global "rating" system. Since this is abstracted and not attached to anything else within your project, say an item to be rated, then the routing must go from the initial rating component -> click on rate -> rating module action (executes rate) -> back to location (executes referrer).

Doing this with AJAX , by just tacking on "/ajax" to the routing requires that the referrer also know that it needs to be referred to its ajax route, this can be solved w/ forwarding function....

etc./