![]() |
|
Snippets |
|
I made a small more general modification to the edit_in_place update action, you can use that in any action
class myTools { /** * performs update on any single column for ajax actions * * @param string $peer * @param integer $id * @param string $field * @return object */ public static function updateField($peer, $id, $field, $value) { if (!class_exists($peer)) { throw new InvalidArgumentException($peer.' does not exist'); } $method = new ReflectionMethod($peer, 'retrieveByPk'); $object = $method->invoke(NULL, $id); $object->setByName($field, $value, BasePeer::TYPE_FIELDNAME); $object->save(); return $object; }
One stumbler for me was that I didn't realize that the form created by the input_in_place_editor_tag() helper only submits one key/value pair (value=xxx), so to tell the action which PK to use or which object field to update, you have to pass that information on the form URL (field and id).
One way to solve it would be to write a different action for each field you want to update, but I wanted something I could reuse without writing additional actions, so I added two GET parameters:
So, in my showSuccess.php template:
<h1 id="eipTitle"><?php echo $comment->getTitle() ? $comment->getTitle() : "click to edit" ?></h1> <?php echo input_in_place_editor_tag( 'eipTitle', 'comment/ajaxUpdate?field=title&commentid='.$comment->getCommentid(), array( 'cols' => 40, 'rows' => 1, )) ?>
BTW, you can read about the valid options you can pass in the third argument to input_in_place_editor_tag() at the script.aculo.us Wiki
And for the action, I made it as generic as possible (actually it would be nice if Propel generated an action like this next to the regular executeUpdate action):
private function raiseEipError ($message) { $this->logMessage($message, 'err'); return $this->renderText( "ERROR: $message" ); } public function executeAjaxUpdate() { $id = $this->getRequestParameter('commentid'); $field = $this->getRequestParameter('field'); $this->logMessage("commentid:$id and field='$field'", 'debug'); // Check for required params. // Return a nice message to the user and in the log if there's a problem. if (! $field) return $this->raiseEipError( "No field parameter passed in form action" ); $valid_fields = CommentPeer::getFieldNames(BasePeer::TYPE_FIELDNAME); if (! in_array($field, $valid_fields)) return $this->raiseEipError( "Invalid field parameter '$field' passed in form action.\n". " Valid fields: (" . implode(", ", $valid_fields) . ")" ); if (! $id) return $this->raiseEipError( "No ID parameter passed in form action" ); $comment = CommentPeer::retrieveByPk($id); if (! $comment) return $this->raiseEipError( "Object with ID $id not found" ); $comment->setByName($field, $this->getRequestParameter('value'), BasePeer::TYPE_FIELDNAME); $comment->save(); return $this->renderText( $comment->getByName($field, BasePeer::TYPE_FIELDNAME) ); }
One stumbler for me was that I didn't realize that the form created by the input_in_place_editor_tag() helper only submits one key/value pair (value=xxx), so to tell the action which PK to use or which object field to update, you have to pass that information on the form URL (field and id).
One way to solve it would be to write a different action for each field you want to update, but I wanted something I could reuse without writing additional actions, so I added two GET parameters:
So, in my showSuccess.php template:
<h1 id="eipTitle"><?php echo $comment->getTitle() ? $comment->getTitle() : "click to edit" ?></h1> <?php echo input_in_place_editor_tag( 'eipTitle', 'comment/ajaxUpdate?field=title&commentid='.$comment->getCommentid(), array( 'cols' => 40, 'rows' => 1, )) ?>
BTW, you can read about the valid options you can pass in the third argument to input_in_place_editor_tag() at the script.aculo.us Wiki
And for the action, I made it as generic as possible (actually it would be nice if Propel generated an action like this next to the regular executeUpdate action):
private function raiseEipError ($message) { $this->logMessage($message, 'err'); return $this->renderText( "ERROR: $message" ); } public function executeAjaxUpdate() { $id = $this->getRequestParameter('commentid'); $field = $this->getRequestParameter('field'); $this->logMessage("commentid:$id and field='$field'", 'debug'); // Check for required params. // Return a nice message to the user and in the log if there's a problem. if (! $field) return $this->raiseEipError( "No field parameter passed in form action" ); $valid_fields = CommentPeer::getFieldNames(BasePeer::TYPE_FIELDNAME); if (! in_array($field, $valid_fields)) return $this->raiseEipError( "Invalid field parameter '$field' passed in form action.\n". " Valid fields: (" . implode(", ", $valid_fields) . ")" ); if (! $id) return $this->raiseEipError( "No ID parameter passed in form action" ); $comment = CommentPeer::retrieveByPk($id); if (! $comment) return $this->raiseEipError( "Object with ID $id not found" ); $comment->setByName($field, $this->getRequestParameter('value'), BasePeer::TYPE_FIELDNAME); $comment->save(); return $this->renderText( $comment->getByName($field, BasePeer::TYPE_FIELDNAME) ); }
If you want to use a TinyMCE editor in a form called by ajax, you have to make some additional changes to the code. It's not complicated.
In view.yml include TinyMCE and Prototype:
javascripts: [/sf/js/prototype/prototype.js, /js/tiny_mce/tiny_mce.js]
In your main decoration template include the javascript code bellow (best at the end of the file). In most cases this code will be the layout.php.
<?php echo javascript_tag(' var activatedAreas = new Array(); function setTextareaToTinyMCE(sEditorID) { var oEditor = document.getElementById(sEditorID); if(oEditor) { if(activatedAreas[sEditorID] == true) unsetTextareaToTinyMCE(sEditorID); try { activatedAreas[sEditorID] = true; tinyMCE.execCommand("mceAddControl", true, sEditorID); } catch(e) { alert("Error activating element " + sEditorID + "\n" + e); } } } function unsetTextareaToTinyMCE(sEditorID) { var oEditor = document.getElementById(sEditorID); if(oEditor && (activatedAreas[sEditorID] == true)) { try { tinyMCE.execCommand("mceRemoveControl", true, sEditorID); activatedAreas[sEditorID] = false; } catch(e) { alert("Error deactivating element " + sEditorID + "\n" + e); } } } function tinymceDeactivate() { try { tinyMCE.triggerSave(true,true); } catch(e) { alert("Error saving form\n" + e); } for(var i in activatedAreas) { if(activatedAreas[i] == true) { unsetTextareaToTinyMCE(i); } } } function submitForm(formId) { if($(formId).onsubmit()) { $(formId).submit(); } } tinyMCE.init({ theme : "advanced", language : "en", mode : "exact", relative_urls : false, debug : false, valid_elements : "*[*]", height:"100%", width:"100%", theme_advanced_resize_horizontal : false, theme_advanced_resizing : true, auto_reset_designmode : true }); ') ?>
And now finally in the action template which is called by ajax, use the code fragment bellow. The link_to_remote call has to have script => true.
Form:
<form id="form_id"> ...... <?php echo textarea_tag('article','article','size=10x30'); ?> ...... </form>
At the end of the action template (e. g. editSuccess.php):
<a class="button save" alt="SAVE" href="#" onclick="tinymceDeactivate();submitForm('form_id'); return false">SAVE</a> <?php echo javascript_tag(' setTextareaToTinyMCE("article"); ') ?>
The important thing was to deactivate the tinymce editor on textarea before submitting the form, otherwise there were errors after re-appearing of the form.
The example is for a blog. The page that displays a post also proposes an AJAX form to add a comment. We want that when the validation of this form fails, it displays again in the page with an error message, and when the validation succeeds, the form is replaced byu the comment just posted.
The idea is to take advantage of the way the update option of the form_remote_tag() helper works. It accepts an associative array, where you can specify different zones to update in case of success and failure. The only problem is that for Prototype, a failure is a return code other than 2XX. So when we return the form showing the error message again, we need to set the status code to 404, for instance, for Prototype to choose to update the correct zone.
That, plus the usual use of partials here and there, and you have a working solution:
in modules/post/actions/action.class.php
// Display the form public function executeShow() { $this->post = PostPeer::retrieveByPk($this->getRequestParameter('post_id')); }
in modules/post/templates/showSuccess.php
// Display question detail here ... // Beginning of Comment zone <div id="added_comment" style="display_none"> </div> <div id="add_comment"> <?php include_partial('comment/add_comment', array('post' => $post)) ?> </div>
in modules/comment/templates/_add_comment.php
<?php use_helper('Javascript', 'Validation') ?> <?php echo form_remote_tag(array( 'url' => 'comment/add', 'update' => array('success' => 'added_comment', 'failure' => 'add_comment'), 'script' => true, 'loading' => "Element.show('indicator')", 'success' => "Element.hide('indicator');Element.show('added_comment');Element.hide('add_comment');", 'failure' => "Element.hide('indicator');", )) ?> <?php echo input_hidden_tag('post_id', $post->getId()) ?> <?php echo form_error('body') ?> <label for="body">Your comment</label> <?php echo textarea_tag('body') ?> <?php echo submit_tag('Send') ?> </form>
in modules/comment/validate/add.yml
methods: post: [body] fillin: activate: Yes names: body: required: Yes required_msg: You must provide a comment validators: spamValidator spamValidator: class: sfRegexValidator param: match: No pattern: /http.*http/ match_error: Do not provide more than one URL - It is considered Spam
in modules/comment/actions/action.class.php
public function handleErrorAdd() { $this->post = PostPeer::retrieveByPk($this->getRequestParameter('post_id')); $this->getResponse()->setStatusCode(404); return sfView::ERROR; } public function executeAdd() { $post = PostPeer::retrieveByPk($this->getRequestParameter('post_id')); $this->forward404Unless($post); $comment = new Comment(); $comment->setPost($post); $comment->setAuthor($this->getUser()->getAuthor()); $comment->setBody($this->getRequestParameter('body')); $comment->save(); $this->comment = $comment; }
in modules/comment/templates/addError.php
<?php include_partial('comment/add_comment', array('post' => $post)) ?>
in modules/comment/templates/addSuccess.php
Your comment has been added: <div class="comment"> <?php echo $comment->getBody() ?> </div>
As a bonus, the form is still there after a successful submission (but hidden), so with a few more lines of code, you can still provide a Digg-like "edit comment for the next 60 seconds" feature.
In the template where the object that users can vote on (here, a snippet) is displayed, add:
<?php echo use_helper('Javascript') ?> <?php $id = $snippet->getId() ?> <?php if($sf_user->canVoteFor($snippet)): ?> <span id="vote_for_<?php echo $id ?>" style="white-space:nowrap;"> rate this snippet: <?php for($i = 1 ; $i <= 5 ; $i++): ?><div class="rate <?php if($i <= $snippet->getAverageVote()): ?>rated<?php endif; ?>" id="vote_<?php echo $id ?>_<?php echo $i ?>"><?php echo link_to_remote(image_tag('spacer.gif', 'width=10 height=10'), array( 'url' => 'snippet/voteForSnippet?id='.$id.'&vote='.$i, 'update' => 'vote_for_'.$id, 'loading' => visual_effect('appear', 'indicator'), 'complete' => visual_effect('fade', 'indicator').visual_effect('highlight', 'vote_for_'.$id), ), array( 'onMouseOver' => 'highlight_stars('.$id.', '.$i.', true);', 'onMouseOut' => 'highlight_stars('.$id.', '.$i.', false);', )) ?></div><?php endfor; ?> <span id="indicator" style="display:none"> <?php echo image_tag('indicator.gif') ?></span> </span> <?php else: ?> <?php include_partial('voted', array('id' => $id, 'vote' => $snippet->getAverageVote())) ?> <?php endif; ?>
Of course, you have to define the rules about who can vote on what in the ->canVoteFor() method of the User object (in apps/myapp/lib/myUser.php).
The template uses a _voted.php partial:
<?php for($i = 1 ; $i <= 5 ; $i++): ?><div class="rate <?php if($i <= $vote): ?>rated<?php endif; ?>" id="vote_<?php echo $id ?>_<?php echo $i ?>"><img src="/images/spacer.gif" width="10" height="10" /></div><?php endfor; ?>
DO NOT add extra spaces to the long lines, or the stars get separated by blank spaces.
The mechanism that changes the aspect of stars relies on classes, so add the following styling to a CSS used in your template:
.rate { display:inline; width:10px; height:10px; margin:0; padding:0; background-image:url(/images/vote_star.gif); background-position: left 10px; } .rated { background-position: left 0px; } .ratehover { background-position: left 20px; }
The vote_star.gif file is a 10x30px image containing three versions of the star: voted, hovered, not voted.

The snippet/voteForSnippet action does something like:
public function executeVoteForSnippet() { $this->id = $this->getRequestParameter('id'); $snippet = SnippetPeer::retrieveByPk($this->id); if(!$snippet) { return sfView::NONE; } $vote = new Vote(); $vote->setUserId($this->getUser()->getUserId()); $vote->setSnippetId($snippet->getId()); $vote->setVote($this->getRequestParameter('vote')); $vote->save(); $this->vote = $vote->getSnippet()->getAverageVote(); }
I've found it annoying to set the show and hide commands on each remote request. So I've used this to define it globally:
/* * Defines the indicator globally */ Ajax.Responders.register({ onCreate: function() { Element.show('indicator'); }, onComplete: function() { Element.hide('indicator'); } });
creates an inplace editable html table from a database table. save this helper in your_project/lib/helper/input_in_place_editor_gridHelper.php dir
Special thanks to Christian.
There is a problem with saving the snippet here.I will try to update it later.
But for a normal (user interface) following action can be used:
public function executeUpdategrid() { $value=trim(strip_tags($_POST['value']));// new value of the cell being updated $pFieldValue=intval($this->getRequestParameter('pFieldValue'));// value of primary key of the record in the table, ie id value $fieldNo=intval($this->getRequestParameter('fieldNo'));// index of the field coming from input_in_place_grid_tag /* to permit the fields just we want to update here 2,3 and 4 are indexes of these fields in $rs, and this is for security this is used only if secure parameter is set in grid options */ # these are just examples to illustrate secure usage if($fieldNo==2) $fieldName='Account.FIRSTNAME'; elseif($fieldNo==3) $fieldName='Account.LASTNAME'; elseif($fieldNo==4) $fieldName='Account.BIRTHD'; elseif($fieldNo==5) $fieldName='Settings.EMAIL'; else die("Invalid Table Field!");// invalid field number.(just for security.Complete table field names can be used in an admin application) $split=explode(".",$fieldName); $tableName=$split[0]; // to find primary key field name $fields=call_user_func(ucfirst(strtolower($tableName)).'Peer::getFieldNames'); if($fields[0]) { # primary key field name $pFieldName=$tableName.".".strtoupper($fields[0]); # update corresponding field $conn=Propel::getConnection(); $sql="UPDATE $tableName SET $fieldName='$value' WHERE $pFieldName=$pFieldValue"; $conn->executeQuery($sql); $this->value=$value;// set the value to print out in the template "updategridSuccess.php" } else $this->value=null; }
or both can be used in same action as follows:
/** * account actions. * * @package 1insaat * @subpackage account * @author Ahmet Ertek * @version 1.0 * * @desc Implements input_in_place_editor_grid helper's cell update.(this helper is not standart, just a custom helper).This action is not in use now.See /profile/templates/settingsSuccess.php */ public function executeUpdategrid() { $value=trim(strip_tags($_POST['value']));// new value of the cell being updated $pFieldValue=intval($this->getRequestParameter('pFieldValue'));// value of primary key of the record in the table, ie id value $fieldNo=intval($this->getRequestParameter('fieldNo'));// index of the field coming from input_in_place_grid_tag $fieldName=trim($this->getRequestParameter('fieldName'));// name of the field to be updated $fieldName=str_replace("_",".",$fieldName);// replace _ with. This is because no_script_name can be setto "on" in settings.yml of app /* to permit the fields just we want to update here 2,3 and 4 are indexes of these fields in $rs, and this is for security this is used only if secure parameter is set in grid options */ if(!$fieldName) { # these are just examples to illustrate secure usage, change these field names with yours if($fieldNo==2) $fieldName='Account.FIRSTNAME'; elseif($fieldNo==3) $fieldName='Account.LASTNAME'; elseif($fieldNo==4) $fieldName='Account.BIRTHD'; elseif($fieldNo==5) $fieldName='Settings.EMAIL'; else die("Invalid Table Field!");// invalid field number.(just for security.Complete table field names can be used in an admin application) } $split=explode(".",$fieldName); $tableName=$split[0]; // to find primary key field name $fields=call_user_func(ucfirst(strtolower($tableName)).'Peer::getFieldNames'); if($fields[0]) { # primary key field name $pFieldName=$tableName.".".strtoupper($fields[0]); # update corresponding field $conn=Propel::getConnection(); $sql="UPDATE $tableName SET $fieldName='$value' WHERE $pFieldName=$pFieldValue"; $conn->executeQuery($sql); $this->value=$value;// set the value to print out in the template "updategridSuccess.php" } else $this->value=null; } ?>
Note that to use this grid for admin app you may use
$options['secure']=>false;
If you need to call a remote function with the parameter of the changed select .
Nota for François : Could be cool to have a better documentation of remote_function.
Here is the editSuccess.php
<tr> <th> <?php echo __('Domains:') ?> </th> <td> <?php echo form_error('domain') ?> <?php echo select_tag('domain', objects_for_select( $domains, 'getIdDomain', 'getName', $list->getDomain(), 'include_custom='.__('Choose a domain')), array( 'onchange' => remote_function(array( 'update' => 'item_domain', 'url' => 'list/subdomain', 'with' => "'id=' + this.options[this.selectedIndex].value" )) ) ) ?> </td> </tr> <tr> <th> <?php echo __('Sub domains:')?> </th> <td> <?php echo form_error('sub_domain') ?> <div id="item_domain"> <?php echo select_tag('sub_domain', objects_for_select( $list_sub_domains, 'getIdDomain', 'getName', $list->getSubDomain(), 'include_custom='.__('Choose a sub domain') )) ?> </div> </td> </tr>
Here is the subdomainSuccess.php
<?php use_helper('Object') ?> <?php echo select_tag('sub_domain', objects_for_select( $list_sub_domains, 'getIdDomain', 'getName', 0, 'include_custom='.__('Choose a sub domain') )) ?>
The problem seems to arise quite frequently: a page contains one Ajax link, but the remote function must update several divs on the page. Consider, for instance, the following template:
<h1>First zone to update</h1> <div id="first_zone"> Hello there </div> <h1>Second zone to update</h1> <div id="second_zone"> <p>How do you do, <strong>mate</strong>? </div> <h1>Ajax link</h1> <?php echo use_helper('Javascript') ?> <?php echo link_to_remote('click me', array( 'url' => 'test/ajax', 'update' => 'result', 'script' => true, )) ?> <div id="result"> </div>
What would the test/ajax action look like to update both the first_zone and the second_zone?
For the code of the action itself (executeAjax()), we'll ignore it since it really depends on what logic you put in your Ajax interaction. For this example, it will be empty.
The code of the template (ajaxSuccess.php) can be as follows:
<?php echo use_helper('Javascript') ?> <?php slot('first_update') ?> So you like clicking, uh? <?php end_slot() ?> <?php slot('second_update') ?> <p>I'd like to test quotes (like "). </p> <p>And <strong>tags</strong>, too.</p> <?php end_slot() ?> <?php echo javascript_tag( update_element_function('first_zone', array( 'content' => get_slot('first_update'), )) . update_element_function('second_zone', array( 'content' => get_slot('second_update'), )) ) ?>
Once rendered, the HTML code sent to the user will look like this:
<script type="text/javascript"> //<![CDATA[ $('first_zone').innerHTML = ' So you like clicking, uh?\n'; $('second_zone').innerHTML = ' <p>I\'d like to test quotes (like \"). </p>\n <p>And <strong>tags</strong>, too.</p>\n'; //]]> </script>
And this will do exactly what we wanted: update both zones with different content, in a single remote call. The interest of using the slot helpers is that you don't need to worry about escaping the content passed to the JavaScript function, and it looks really nice in your favorite syntax-highlighting text editor.
If you happen to do this a lot, maybe you will want to package the multiple updater into a helper. That's quite easy:
function update_elements_function($updates) { $res = ""; foreach($updates as $zoneName => $slotName) { $res .= update_element_function($zoneName, array('content' => get_slot($slotName))); } return javascript_tag($res); }
Then you could replace the call to the javascript_tag() in the ajaxSuccess.php template by a simpler:
<?php echo update_elements_function(array( 'first_zone' => 'first_update', 'second_zone' => 'second_update', )) ?>
Performancewise, if the Ajax response is small (below 512 Bytes), you'd better use the JSON approach.
You can pass aditional parameters to input_auto_complete_tag helper. By default, autocompleter pass only object-referer value. For aditional parameter you need use 'with' in $completion_options array. in view layer:
< <?php echo object_input_tag($model, 'getFilter', array ('size' => 20, 'control_name' => 'filter',));?> ?php echo input_auto_complete_tag('field_to_search','','module/autocomplete', array('autocomplete'=>'off'),array('use_style'=>true,'with'=> " value+'&filter='+$('filter').value"))?>
you must put '" value' (with whitespace) because javascript fail (a little symfony bug). Aditional parameters must go after value.
in control layer:
public function executeAutocomplete(){ $search=$this->getRequestParameter('field_to_search'); $filter=$this->getRequestParameter('filter'); .... }
now you can use field_to_search and filter to construct any query in model layer ans show this in AutocompleteSuccess.php view or another view that you define. this was tested in synfony version > 0.9. Check your version, and review your javascripthelper helper to verify if your input_auto_complete_tag helper supports 'with' option
This snippet is usefull to handle a session timeout in an ajax request.
It is very bad to have the login page filling the update div zone...
The idea came from a post in the forum (thanks a lot RoVeRT !)
The method is :
The security module redirect the request to the login action.
The login action send a 401 http error code if it detects an ajax request.
The ajax helper handle the 401 error with a javascript function which display a popup and redirect to the full login page.
For that purpose, we will add a little code in the ajax helper and in the login action.
1 - We handle the 401 error code in the ajax helper and enable javascript execution for the popup :
401 => "if ( confirm('Your not logged anymore... Ok to go to the login page.')) {document.location='/';}",
2 - We add this code at the beginning of the login action :
// if the request is an ajax request... if ($this->getRequest()->isXmlHttpRequest()) { // response to the ajax request : code http 401 (access unauthorized) $this->getResponse()->setStatusCode(401); }
Thanks for the comments !
If you are updating your interface with ajax actions, typically users have no way to bookmark or return to the page with those updates already in place. This is seen as a fundamental problem with ajax: the URL shown at the top of the browser no longer constitutes a complete specification of the information shown in the window.
This snippet explains how you can track and recreate the current view even when using ajax to update your page. Now if users reload the page or send this link to a friend, the current state will remain intact. (You can view an example of this technique on http://www.ask.com/)
The trick relies on two parts:
First, you must allow the links created using the helpers link_to_remote and link_to_function to NOT have a 'return false;' appended to the end of the onclick attribute of the tag that is generated. There is no way to change this by way of the default helper, but it's easy to override with our own.
A 'return false' statement means the browser won't try to navigate to the url in the 'href' attribute of your link. Usually you don't want your browser window to navigate to the default "href" link--you are just creating the link as means to make the ajax call. Typically href attributes, if any, are present in case the user does not have javascript enabled.
However, we do not want "return false" appended to the onclick attribute. Ideally our helper function would allow us to do something like this:
<?php echo my_link_to_remote('Read this post', array( 'update' => 'indicator', 'url' => 'post/read?id='.$post->getId(), 'return' => 'true', ), array('href' => '#post:read|id:'.$post->getId()) ?>
which creates this:
<a href="#post:read|id:1234" onclick="new Ajax.Updater(....); return true;">Read this post</a>
When a user clicks the link, not only is the ajax action called but the URL of the page gets updated to something like "http://mysite.com/#post:read|id:1234".
So, let's write our own customer helper. I placed it into the /lib/helper directory in a file called myJavascriptHelper.php.
<?php /* File: myJavascriptHelper.php */ /** * Returns a link that will trigger a javascript function using the * onclick handler and return TRUE OR FALSE after the fact * depending on the $html_options['return'] parameter. * This attribute is removed before we use the default content_tag helper. * * Examples: * <?php echo link_to_function('Greeting', "alert('Hello world!')", array('return'=>true)) ?> * <?php echo link_to_function(image_tag('delete'), "if confirm('Really?'){ do_delete(); }") ?> */ function my_link_to_function($name, $function, $html_options = array()) { $html_options = _parse_attributes($html_options); $html_options['href'] = isset($html_options['href']) ? $html_options['href'] : '#'; $html_options['onclick'] = $function.'; return '; $html_options['onclick'] .= (isset($html_options['return']) && $html_options['return'] == true) ? 'true;' : 'false;'; unset($html_options['return']); return content_tag('a', $name, $html_options); } /** * See docs on link_to_remote helper for more info. */ function my_link_to_remote($name, $options = array(), $html_options = array()) { return my_link_to_function($name, remote_function($options), $html_options); } ?>
Now for my ajax links I use my new helper and make sure to pass an href attribute as well as the 'return' attributte. This determines if clicking on the link will return true (update the url) or false (don't update it). Here is a real example of how I use it to keep track of what message is currently viewed in a message component within my interface.
<?php echo my_link_to_remote('Load This Message Inline', array( 'update' => 'message_viewer', 'url'=>'message/viewInline?id='.$message->getId().'&view='.$view, 'loading' => "Element.show('message_indicator')", 'complete'=> "viewInlineComplete(".$message->getId().", '".$view."')", 'method'=>'get', ), array( 'href' => '#message:viewInline|id:'.$message->getId().'|view:'.$view, 'return' => true)) ?>
Notice that the href attribute has a string that mirrors the URL attribute. We could probably take the my_link_to_remote helper a step further and automatically generate the HREF attribute based on the URL, but I decided to keep some flexibility there. Now, if a user clicks the link, the url will get a string like "#message:viewInline|id:3|view:inbox" appended to it.
In my experience, both IE 6 and FF did NOT scroll to the top of the window nor did the page reload. In fact I believe the W3 DOM specs state that links to a hash address never reload the document. Instead, I get a nice new attribute in the URL of the page. Now, if my user wants to bookmark the page, or perhaps reload it, the current ajax state can be recalled using the information in the anchor tag.
Although you cannot retrieve the value after the hash from within PHP, you can retrieve it client-side by way of the 'location.hash' function and then set a cookie, update the page, or do whatever you like based on those attributes. I am using it to reload a message from a users inbox into the inline viewer. Let's see how it works.
First, we need a way to load in the hash string and turn it into something we can use. Here is the function I use and included in my main.js file that is used throughout my site.
function readHashVars() { var hash = window.location.hash; hash = hash.substring(1); hash = hash.replace(/\|/g, '&'); hash = hash.replace(/\:/g, '='); // lets turn our url hash into a javascript hash (like an associated array) // we will use a useful function provided in the prototype library hash = hash.parseQuery(); return hash; }
Note: This function requires the prototype library to work!
Next, whenever my page is loaded I should check to see if the hash contains attributes I set within my ajax actions. If there are, I call the same client-side update from remote function as I do when a user clicks on the ajax link in the interface. Here is the top of my indexSuccess.php file for my home page action. I do not include this within the main.js file because I want to use the PHP helpers like remote_function provided by symfony. Compare this to the use of my_link_to_remote in the code above.
<?php use_helper('Javascript') ?> <?php echo javascript_tag(" function updateDashboard() { var hash = readHashVars(); // we now have a hash with attributes matching the parameters in the url // so if the url has #message:viewInline|id:3, we have a javascript object like // hash { message: viewInline, id: 3 } if(hash.message == 'viewInline') { " . remote_function(array( 'update' => 'message_viewer', 'url' => 'message/viewInline', 'with' => "'id='+hash.id+'&view='+hash.view", 'loading' => "Element.show('message_indicator')", 'complete'=> 'viewInlineComplete(hash.id, hash.view)', 'method' => 'get', )) . " } } Event.observe(window, 'load', updateDashboard, false); ") ?>
This time, instead of calling the "remote_function" helper with parameters within PHP, I rely on javascript to send the correct parameters using the "with" parameter. That way, I can include values from javascript that aren't available until the page is loaded.
That's all there is to it (phew!). Now that this is all set up, I can easily convert my inline ajax links to javascript functions that are automatically called when the necessary attributes exist within the URL hash.
What is worth noting in the snippet below is the usage of the 'onchange'=> remote_function( ... you can use the remote_function in many places to call other functions ... like in the "update", "complete" etc...
<div id="js_updating">Stand by..</div> <?php echo form_tag('/module/action', 'method=get class=simpleForm') ?> Please select the day: <?php echo input_date_tag('day', 'now', array('rich' => true, 'readonly'=>true, 'onchange'=> remote_function( array( 'update' => 'Area To Update (DIV TAG)', 'url' => 'MODULE/ACTION', 'loading' => "Element.show('js_updating')", 'complete' => "Element.hide('js_updating')" )))); ?> </form>
One link, multiple ajax div updates, the ultimate solution
I already proposed a solution in another snippet, but it seems I missed the simplest solution.
Let's summarize the probkem again: A page contains one Ajax link, but the remote function must update several divs on the page. Consider, for instance, the following template:
<h1>First zone to update</h1> <div id="first_zone"> Hello there </div> <h1>Second zone to update</h1> <div id="second_zone"> <p>How do you do, <strong>mate</strong>? </div>
We want an ajax link to update the two zones. The solution is not to use the Ajax helpers, but rather call the prototype Ajax object directly:
<?php echo use_helper('Javascript') ?> <h1>Ajax link</h1> <?php echo link_to_function('click me', 'new Ajax.Request(\''.url_for('test/ajax').'\');return false')) ?>
The code of the action itself (executeAjax()) does some server stuff to prepare the data used to update the template. It really depends on what logic you put in your Ajax interaction. For this example, it will be empty.
The code of the template (ajaxSuccess.php) just needs to be as follows:
<?php $sf_context->getResponse()->setContentType('text/javascript') ?> <?php slot('first_update') ?> So you like clicking, uh? <?php end_slot() ?> <?php slot('second_update') ?> <p>I\'d like to test quotes (like "). </p> <p>And <strong>tags</strong>, too.</p> <?php end_slot() ?> Element.update('first_zone', '<?php include_slot('first_update') ?>'); Element.update('second_zone', '<?php include_slot('second_update') ?>');
And that's it. Because the response content type is text/javascript, the Ajax object will eval it automatically (no need to mention evalScripts: true anymore).
And this will do exactly what we wanted: update both zones with different content, in a single remote call. The interest of using the slot helpers is that you don't need to worry about escaping the content passed to the JavaScript function, and it looks really nice in your favorite syntax-highlighting text editor.
I have:
<?php echo link_to_remote("update news", array( 'update' => 'news_zone', 'url' => 'news/index', ) ) ?>
The div with the id 'news_zone' will be updated if i click on the link 'update news'. But how can i update another div ?
You have to add the code in the file symfony/helper/JavascriptHelper.php:
function remote_function($options) { sfContext::getInstance()->getResponse()->addJavascript('/sf/js/prototype/prototype'); // Multi ajax call if (isset($options[0]) && is_array($options[0])) { $multi_function = ""; // First pass (wait) for ($i = 0; isset($options[$i]); $i++) { if (isset($options[$i]['wait']) && $options[$i]['wait'] != $i && isset($options[$options[$i]['wait']])) { if (isset($options[$options[$i]['wait']]['complete'])) { $options[$options[$i]['wait']]['complete'] .= '; ' . remote_function($options[$i]); } else { $options[$options[$i]['wait']]['complete'] = remote_function($options[$i]); } } } // Second pass for ($i = 0; isset($options[$i]); $i++) { if (!(isset($options[$i]['wait']) && $options[$i]['wait'] != $i && isset($options[$options[$i]['wait']]))) { $multi_function .= remote_function($options[$i]) . ';'; } } return $multi_function; } $javascript_options = _options_for_ajax($options); ...
Now you can use a new syntax (This patch won't affect the "old" syntax. You can use it):
<?php echo link_to_remote('multi ajax', array( array('update' => 'news_zone', 'url' => '/news/index' ), array('update' => 'second_zone', 'url' => '/module/action' ) ) ); ?>
I provide a special feature: "ajax in order". Requests are asynchronous. Now, you can set an order:
<?php echo link_to_remote('multi ajax', array( array('update' => 'news_zone', 'url' => '/news/index' ), array('update' => 'second_zone', 'url' => '/module/action', 'wait' => 0 /* index table. So update after 'news_zone' */