![]() |
|
Snippets |
|
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.
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' */ ) ) ); ?>
Quentin Garnier, in this snippet: http://www.symfony-project.com/snippets/snippet/57, suggested a way to have a link call multiple remote functions (with "in order" calls being enforced by a new 'wait' parameter). With a slightly modified version of his code, here are a few more functions to be added to you Javascript.php function in the symfony core:
function periodically_call_remotes($options = array()) { $frequency = isset($options[0]['frequency']) ? $options[0]['frequency'] : 10; // every ten seconds by default $code = 'new PeriodicalExecuter(function() {'.remote_functions($options).'}, '.$frequency.')'; return javascript_tag($code); } function link_to_remotes($name, $options = array(), $html_options = array()) { return link_to_function($name, remote_functions($options), $html_options); } function remote_functions($options) { // Multi ajax call $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; }
To use it, you could do something like this:
<?php echo link_to_remotes('multi ajax', array( array( 'update' => 'first_zone', 'url' => '/module/action1', 'loading' => "Element.show('indicator')" ), array( 'update' => 'second_zone', 'url' => '/module/action2', 'wait' => 0 /* update in a second server call, after 'news_zone' returns */ ), array( 'update' => 'third_zone', 'url' => '/module/action3', 'wait' => 0 ), array( 'update' => 'fourth_zone', 'url' => '/module/action4', 'complete' => "Element.hide('indicator')", 'wait' => 2 /* update in a third server call, after 'third_zone' returns */ ) ) ) ?>
or this:
<?php echo periodically_call_remotes('multi ajax', array( array( 'frequency' => 60 /* every 60 seconds, frequency only needs to be specified in first call, not subsequent calls */ 'update' => 'first_zone', 'url' => '/module/action1', 'loading' => "Element.show('indicator')" ), array( 'update' => 'second_zone', 'url' => '/module/action2', 'wait' => 0 /* update in a second server call, after 'news_zone' returns */ ), array( 'update' => 'third_zone', 'url' => '/module/action3', 'wait' => 0 ), array( 'update' => 'fourth_zone', 'url' => '/module/action4', 'complete' => "Element.hide('indicator')", 'wait' => 2 /* update in a third server call, after 'third_zone' returns */ ) ) ) ?>
Notes
It should be noted that using the 'wait' parameter will cause all top level ajax remote function calls to be executed in one call to the server, and each second level to be executed in a second call, etc. Basically, multiple calls to the server are generated by the user action.
This will cause huge performance issues if you attempt to use these functions in a high-traffic scalable-sensitive system. It is mainly intended for a link which executes two or three functions at most. To do more, you should consider using another approach, like the JSON approach documented in the wiki.
This helper creates a div tag tag into which links can call other actions and dynamically display its output via AJAX. When JS is not enabled, the links reload the page with GET parameters and allow this helper to load the output.
What do you think of this code? First of all, has it already been done before? And do you think it will actually be useful? How well does it conform to the MVC structure?
Actual code:
<?php /** * This file includes functions that assist in making AJAX degradable * * These small helper functions add in some of the basic features that allow for degradable * JS. The idea is to dynamically load contents when JS is available and let the page reload with * appropriate html when JS is not. * @author Yining Zhao * @package YZ_Helpers * @subpackage AJAX_Degredation * @since 1/16/2007 */ /** * This class creates a pair of <div> tags in which AJAX functions can input dynamic HTML. * * Usually what happends is that a link is pressed to activate the AJAX command via its onClick method. * The HREF attribute is suppressed by a 'return false;' at the end of the onClick method. However * when JS is not available, the onClick method is not activated and thus the HREF is called. The * link in the HREF reloads the page and uses GET to pass along the variables to load the designated * fragment. * * If the $trigger_var is 'login_box', here is what the following GET parameters will do: * login_box_display will load the fragment if it is set to 'on', otherwise div style = visibility:none and nothing is loaded * e.g. URL: www.website.com/file.php?login_box_display=on //will load fragment * * login_box_make_persistant will let set login_box_display to 'on' in the user session, so as long as * login_box_make_persistant is not set to 'no', the fragment will automatically be loaded on every page * load regardless of the GET statement * e.g. URL: www.website.com/file.php?login_box_make_persistant=on //will load fragment on every page load * * Sample usage of function: * //This function is being called from listSuccess.php template file in my Post module * div_fragment_creater('login_box','partial','loginPartial',array('referer'=>$referer),array('id'=>'login_div_id')) * * The source: {@source} * * @param string $trigger_var The variable passed via GET that determines if the partial is loaded * @param string $fragment_type This tells the function if you are using a partial or a component * @param string $fragment_filename The name of the fragment to be called * @param array $fragment_values_ary This passes in the values for the fragment * @param array $div_attr_ary This contains all the html attributes for the <div> tag * @return void Since this function is designed to echo the necessary html, return is void */ function div_fragment_creater($trigger_var,$fragment_type, $fragment_filename,$fragment_values_ary,$div_attr_ary) { $sf_user=sfContext::getInstance()->getUser()->getAttributeHolder(); $sf_params=sfContext::getInstance()->getRequest()->getParameterHolder(); //If in the GET there is a make_persistant command for the given trigger variable, then the display command for the //tigger is set for sessions if($sf_params->get($trigger_var.'_make_persistant')=='on') { $sf_user->set($trigger_var.'_display','on'); } else if($sf_params->get($trigger_var.'_make_persistant')=='off') { $sf_user->remove($trigger_var.'_display'); $sf_params->remove($trigger_var.'_display'); } //If the display is set for the trigger in either sf_params or sf_user, display the fragment. //Otherwise, don't display the fragment and make the div attribute style = display:none $output_fragment=false; if(($sf_params->get($trigger_var.'_display')=='on')||($sf_user->get($trigger_var.'_display')=='on')) { $output_fragment=true; } else { $div_attr_arry['style'] = 'display:none'; } $div_attr_keys = array_keys($div_attr_ary); $div_attr_ary_size = count($div_attr_keys); $div_attr=''; $div_attr_collection=''; //Cteates the attributes for the <div> for($i=0;$i<$div_attr_ary_size;$i++) { //This creates a string where the div attribute is set to its corresponding value; e.g. id=5 $div_attr_name = $div_attr_keys[$i]; $div_attr_collection.=$div_attr_name . ' = "' . $div_attr_ary[$div_attr_name] . '" '; } echo '<div '.$div_attr_collection.' >'; if($output_fragment==true) { switch($fragment_type) { case 'partial': echo include_partial($fragment_filename, $fragment_values_ary); break; case 'component': use_component($fragment_filename, $fragment_values_ary); break; } } echo '</div>'; } ?>
Usage Example: In myApp/myModule/list.php:
<body> Anything is possible... <?php //Creates div with id = myDiv. Usually this div is invisible and empty. But when the get or session variable of triggerVar_display is set to 'on', this div becomes visible and displays the whatsPossiblePartial partial file. div_fragment_creater('triggerVar','partial','whatsPossiblePartial',array('id'=>$id),array('id'=>'myDiv')) //Provides link to load action into myDiv. If JS is not enabled, link will reload page with triggerVar_display set to 'on' echo link_to_remote('Click To See Whats Possible', array( 'url'=>'MyModule/WhatPossible?id='.$value, 'update'=>array('success' => 'myDiv'), 'loading'=>"Element.show('indicator')", 'complete'=>visual_effect('blind_down','myDiv',array('duration'=>0.5))';', ), array('href'=>'list?triggerVar_display=on')); ?> </body>
Inside the whatPossibleSuccess.php:
<html> ... <?php //This file is basically just a wrapper for the whatsPossiblePartial. the reason I have this wrapper is so that I can access it from via link_to_remote() on other pages. echo include_partial(whatsPossiblePartial,array('id'=>$this->id)); ?> ... </html>