This forum is in READ-ONLY mode.
You can look around, but if you want to ask a new question, please use Stack Overflow.

New Degradable AJAX Snipeet

Social code snippet repository

New Degradable AJAX Snipeet

by DrDrakken » Tue Jan 30, 2007 3:27 am

Since the Snipeet Section goes crazy when I append lots of code, I'll append my code here and link it to my Snipeet

I created 2 files, 1 class and one module(with 1 action.class.php) that lets you to easily integrate degradable AJAX many of the Javascript remote functions

Place the yzDegradableAjax.class.php class in /app/lib/ . This file contains all of the functions and methods used.

My Snipeet works on the premise that in the JS helpers such as form_remote_tag, or link_to_remote(), the AJAX request action is called via an OnClick/onSubmit() when JS is enabled. For the sake of our argument, we will call the AJAX request action AMod/Aaction .When JS is not enabled, usually a secondary link given via either the 'href' or 'action' attribute is called. The default secondary link for these actions is '#'.
Through my classes, when JS is not enabled, the secondary link brings you to myapp/yz_degradable_ajax_module/Process_nonajax_page action. All of the pertinant information is sent to that action via the GET method. The process_nonajax_page action will also be passed AMod/Aaction and the varables that the original JS helper was trying to pass to it. AMod/Aaction will be called via getPresentation and the values usually passed to it via POST or GET will be passed via $this->getRequest->getAttributes() because that is one of the only ways to pass values from one action to another. The HTML output from getPresentation will be put into a varable and redirects back to the original page. Once back on the original page, the create_div method intercepts that variable and outputs it to the user.

So to the lay user, when JS is not active, the page dynamically loading the new contents via ajax, reloads the whole page with the desired contents.

Please let me know if this works for you or if my instructions are too confusing and I'll try to clarify. All criticisms are welcome.

Here are the code and examples:
Code: Select all
<?php
/*
 * yzDegradableAjax.class.php contains the source for the yzDegradableAjax class.
 * @author Yining Zhao
 * @package yz_AJAX
 * @subpackage Degradation
 * @since 1/26/2007
 */
/*
 * This class contains many methods and wrappers of existing Symfony JS helper functions
 *
 * The methods are all designed to allow for graceful degradation of AJAX code.
 * The idea is to dynamically load contents when JS is available and let the page reload
 * with appropriate html when JS is not.
 *
 * Most of the wrapper functions work by setting up x_remote_tag, which calls
 * yz_degradable_ajax_module/process_nonajax_page when JS is not available.
 * Thus in order for this class to function, the yz_degradable_ajax_module module
 * is required.
 */

class yzDegradableAjax {

  /*
   * yzDegradableAjax::link_to_remote() is a wrapper for the symfony JS helper link_to_remote()
   *
   * @param mixed ,... All of the inputs are exactly the same as the original link_to_remote(), so substitution will be very simple
   * @static
   * @return string
   */
  public static function link_to_remote()
  {
     $remoteTagFuncArgsAry=func_get_args();
     //The critical information for the Symfony link_to_remote() is located in the first argument
     $tmpAry=$remoteTagFuncArgsAry[1];
     $populationKey=$tmpAry['update'];
     $targetLink=$tmpAry['url'];
     return yzDegradableAjax::x_remote_tag('link_to_remote','3','href',
       $targetLink,$populationKey,$remoteTagFuncArgsAry);
  }
  /*
   * yzDegradableAjax::form_remote_tag() is a wrapper for the usual form_remote_tag()
   * Most of the work for this function is done by {@link yzDegradableAjax::x_remote_tag()}
   * @param mixed ,... The parameters for this function are exactly as the ones for the normal form_remote_tag
   * @static
   * @return string
   */
  public static function form_remote_tag()
  {
     $remoteTagFuncArgsAry=func_get_args();
     //The critical information for the Symfony form_remote_tag() is located in the first argument
     $tmpAry=$remoteTagFuncArgsAry[0];
     $populationKey=$tmpAry['update'];
     $targetLink=$tmpAry['url'];
     return yzDegradableAjax::x_remote_tag('form_remote_tag','2','action',
       $targetLink,$populationKey,$remoteTagFuncArgsAry);
  }
 
  /*
   * yzDegradableAjax::button_to_remote() is exactly the same as yzDegradableAjax::form_remote_tag() and yzDegradableAjax::link_to_remote()
   * @param mixed ,... The parameters for this function are exactly as the ones for the normal button_to_remote
   * @static
   * @return string
   */
  public static function button_to_remote()
  {
     $remoteTagFuncArgsAry=func_get_args();
     $tmpAry=$remoteTagFuncArgsAry[0];
     $populationKey=$tmpAry['update'];
     $targetLink=$tmpAry['url'];
     return yzDegradableAjax::x_remote_tag('button_to_remote','2','href',
       $targetLink,$populationKey,$remoteTagFuncArgsAry);
  } 
 
  /*
   * x_remote_tag() is the heart of most of the wrapper functions for the Symfony remote_tag() type functions.
   * Throughout the documentation of this class, reference to Symfony remote_tag() pertains to the collection of
   * Symfony functions and helpers that use AJAX to to update and refresh a section of the page. These functions
   * include: link_to_remote, form_remote_tag, etc.
   * @param string $remoteTagFuncName The name of the remote_tag() to be called
   * @param integer $urlParamSlotNum The parameter slot number of the URL that points to yz_degradable_ajax_module/process_nonajax_page. If 
   * @param string $urlParamName The name of element in the array whose url we need to change
   * @param string $targetLink This is the module and action that would have been called had there benn JS. Given in form of 'module/action' or '@InternalRoutingName'
   * @param string $populationKey This is the identifying key that will let yz_degradable_ajax_module/process_nonajax_page  trigger populate_section()
   * @param array $remoteTagFuncArgsAry This is the array of the arguments that should be passed to the $remoteTagFuncName function
   * @static
   * @return string
   */
  private static function x_remote_tag($remoteTagFuncName,$urlParamSlotNum,
     $urlParamName,$targetLink,$populationKey,$remoteTagFuncArgsAry)
  { 
    //Since the parameters for the remote tag function will be listed using an array, $urlParamSlotNum
    //begins from 0. However most people using this function, will find it more intuitive to start counting
    //slot numbers from 1. Thus, we must decrease $urlParamSlotNum internally.
    $urlParamSlotNum--;
    //$targetLink is the url that the remote tag function normally calls. Usually it is just Module/Action
    //but sometimes, it also includes some parameters such as Module/Action?param1=Boo&param2=Yah
    //Therefore we first break up the link using '?'
    $targetLinkAry=explode('?',$targetLink);
    //Now we are certain only the path information part of the url is in $targetLinkAry[0]
    //First we need to see if the path is given as an internal link e.g. @homepage or in
    //module/action format
    //if (strchr($targetLinkAry[0],'@') yields false, that means there are no '@' in the
    //$targetLinkAry[0] and thus $targetLinkAry[0] must be in 'Module/Action' format
    $moduleActionInfoAry=array();
    if(strstr ($targetLinkAry[0],'@')==false)
    {
        //return an array with elements module and action
        $moduleActionInfoAry=sfRouting::getInstance()->parse($targetLinkAry[0]);
    } else
    {
      $moduleActionInfoAry=sfRouting::getInstance()->getRouteByName($targetLinkAry[0]);
      //The forth element of the returned array is an array with elements module and action
      $moduleActionInfoAry=$moduleActionInfoAry[4];       
    }
   
    //If $targetLink indeed has extra parameters, e.g. $targetLinkAry[1] has 'param1=Boo&param2=Yah',
    //we would add a '&' in front of it and append it to the end of our $moduleActionLink
    if(count($targetLinkAry)>1)
    {
      $additionalTargetLinkValues='&'.$targetLinkAry[1];
    } else
    {
      $additionalTargetLinkValues='';
    }
    $refererPage=sfContext::getInstance()->getRequest()
     ->getAttribute('yz_degradable_ajax_module_refererPage',sfRouting::getInstance()->getCurrentInternalUri());   
    //$moduleActionLink will be the URL used when the remote tag function is unable to use AJAX
   $moduleActionLink='yz_degradable_ajax_module/process_nonajax_page' .
         '?yz_degradable_ajax_module_populationKey='.$populationKey.
         '&yz_degradable_ajax_module_refererPage='.urlencode($refererPage).
         '&yz_degradable_ajax_module_targetModule='.$moduleActionInfoAry['module'].
         '&yz_degradable_ajax_module_targetAction='.$moduleActionInfoAry['action'].
         $additionalTargetLinkValues;
    //Since these are the arguments for a Symfony remote_tag type function, the
    //arguments that defines the URL for when JS does not work will ALWAYS be in
    //the form of an array,
    //e.g. link_to_remote('Link Name',array('update'=>'divName'...),array('href'='urlForWhenNoJS'));

    //If the the argument that contains the URL we want is already set, we will
    //just add or alter the URL attribute
    if(isset($remoteTagFuncArgsAry[$urlParamSlotNum]))
    {
      $tmpAry = $remoteTagFuncArgsAry[$urlParamSlotNum];
      $tmpAry[$urlParamName] = url_for($moduleActionLink);
      $remoteTagFuncArgsAry[$urlParamSlotNum] = $tmpAry;
    } else //However if the argument that contains our URL is not present, we will create it
    {
      $remoteTagFuncArgsAry[$urlParamSlotNum] = array($urlParamName=>url_for($moduleActionLink));
    }
    return call_user_func_array($remoteTagFuncName,$remoteTagFuncArgsAry); 
  }   
 
  /*
   * The populate_section() returns the contents of a remote action if certain conditions are met.
   * This functions checks sfContext::getInstance()->getRequest()
   * ->getAttribute('yzDegradableAjax_populate_section'.$populationKey) to see
   * if yz_degradable_ajax_module/process_nonajax_page has set it
   * equal to 1. If so, the function will return sfContext::getInstance()->getRequest()
   * ->getAttribute('yzDegradableAjax_populate_section_body_'.$populationKey)
   * @param string $PopulationKey This is the unique id that will let the function return the desired text
   * @return string|void
   * @static
   */
  public static function populate_section($populationKey)
  {
     $populationStatus=sfContext::getInstance()
     ->getController()->getActionStack()->getFirstEntry()
     ->getActionInstance()->getFlash('yzDegradableAjax_populate_section_'.$populationKey,0);
      //$populationStatus=sfContext::getInstance()->getRequest()
     //     ->getAttribute('yzDegradableAjax_populate_section_'.$populationKey,0,'test');
     if($populationStatus==1)
     {
       return sfContext::getInstance()
      ->getController()->getActionStack()->getFirstEntry()
      ->getActionInstance()->getFlash('yzDegradableAjax_populate_section_body_'.$populationKey);
     }
  }
 
  /*
   * The create_div() is a wrapper function for {@link yzDegradableAjax::populate_section()} that creates the head of a <div>
   * Like the form_remote_tag, this function will need to be followed by a closing {@link end_div())
   * @param array $divAttributes The div attributes must contain the id attribute as it is also the $populationKey
   * @static
   * @see yzDegradableAjax::end_div()
   * @return void
   */
   public static function create_div($divAttributes)
   {
       $div_id=$divAttributes['id'];
       $populatedSection=yzDegradableAjax::populate_section($div_id);
       echo tag('div',$divAttributes).$populatedSection;
       ob_start();
   }
   
   /*
    * end_div() complements {@create_div()} by determining whether or not to display the contents in the DIV.
    * All of the contents after {@create_div()} is stored in a buffer, end_div() checks the populationKey
    * to determine if it should output the contents.
    * If $populationKey is 0, that means there was nothing to populate so we can display the contents of the DIV
    * If however $populationKey is 1, that means normally when JS is working, the contents of the DIV would be
    * replaced and thus, contents would not be saved.
    * @param string $div_id Must match the div id as set in create_div()
    * @static
    * @return void
    */
   public static function end_create_div($div_id)
   {
    $contents=ob_get_clean();
    $populationKey = $div_id;
   $populationStatus=sfContext::getInstance()
     ->getController()->getActionStack()->getFirstEntry()
     ->getActionInstance()->getFlash('yzDegradableAjax_populate_section_'.$populationKey,0);
     //$populationStatus=sfContext::getInstance()->getRequest()
     //     ->getAttribute('yzDegradableAjax_populate_section_'.$populationKey,0,'test');
     if($populationStatus==0)
     {
       echo $contents;
     }
     echo '</div>';
   }
   
   /*
    * getRequestParameter() combines getRequest and getAttribute and is used when a variable can come via either method
    * This is usually used in the actions that respond to AJAX requests because while normally file can get its
    * values from getRequest(), when JS is not available, the variables will be sent via getAttribute() in
    * (@link yz_degradable_ajax_moduleActions::executeProcess_nonajax_page())
    * This function behaves just like the usual sfContext::getInstance()->getRequestParamer();
    * @static
    * @return mixed
    * @param string $paramName
    * $param mixed $paramName,... The default value is
    */
   public static function getRequestParameter($paramName)
   {
     $defaultValue=null;
     if(func_num_args()==2)
     {
       $defaultValue=func_get_arg(1);//defaultValue would be the 2nd argument
     }
     $random=rand();
     //Since AJAX will be working for most people, getParameter will be most often used
     $paramValue = sfContext::getInstance()->getRequest()
      ->getParameter($paramName,$random);
      //if $paramValue is == $random, that means $paramName yielded nothing,
      //so then we must use getAttribute
      if($paramValue==$random)
      {
        $paramValue = sfContext::getInstance()->getRequest()
          ->getAttribute($paramName,$defaultValue);
      }
      return $paramValue;       
   }
}
?>


This following action.class.php file should be placed in the yz_degradable_ajax_module . It will be the only file of that module. The module is located in app/module/yz_degradable_ajax_module
Code: Select all
<?php

/**
 * yz_degradable_ajax_module actions works with the yzDegradableAjax.class.php to provide degradable ajax.
 *
 * Normally what happends is that the function remote_tag functions created by yzDegradableAjax will call
 * the process_nonajax_page() when JS is not available.
 *
 * @package yz_AJAX
 * @subpackage Degradation
 * @author     Yining Zhao
 * @version    SVN: $Id: actions.class.php 2692 2006-11-15 21:03:55Z fabien $
 */
class yz_degradable_ajax_moduleActions extends sfActions
{
  /*
   * The process_nonajax_page action receives calls from remote_tag functions when JS does not work.
   * It first converts all the Request parameters into attributes so that other actions that it calls
   * call retrieve the information. It then calls and receives the output from the target modules and
   * actions. The output is stored in yzDegradableAjax_populate_section_body_populationKey in attributtes
   * and page that initially directed to this page is called back. There would usually be a function such
   * as (@link yzDegradableAjax::populate_section()) on that page that displays the information stored
   * in the attributes.
   * @return void
   * $see yzDegradableAjax
   * @see yzDegradableAjax::x_remote_tag()
   */
  public function executeProcess_nonajax_page()
  {
     $populationKey=$this->getRequestParameter('yz_degradable_ajax_module_populationKey');
     $refererPage=urldecode($this->getRequestParameter('yz_degradable_ajax_module_refererPage'));
     $targetModule=$this->getRequestParameter('yz_degradable_ajax_module_targetModule');
     $targetAction=$this->getRequestParameter('yz_degradable_ajax_module_targetAction');
     $requestVarValuesAry=$this->getRequest()->getParameterHolder()->getAll();
     $requestVarNamesAry=array_keys($requestVarValuesAry);
     
     //All of the REQUEST variables are looped and added into setAttributes()
     foreach($requestVarNamesAry as $varName)
     {
       $this->getRequest()->setAttribute($varName,$requestVarValuesAry[$varName]);
     }
     //Target is called and output is stored
     $presentationOutput=$this->getPresentationFor($targetModule, $targetAction);
     //Attributes values are set
     //$this->getRequest()->setAttribute('yzDegradableAjax_populate_section_body_'.
     //  $populationKey,$presentationOutput,'test');
     //$this->getRequest()->setAttribute('yzDegradableAjax_populate_section_'.
     //  $populationKey,1,'test');
     $this->setFlash('yzDegradableAjax_populate_section_body_'.$populationKey,$presentationOutput);
     $this->setFlash('yzDegradableAjax_populate_section_'.$populationKey, 1);
     
     //Original referring action is called back
     $this->redirect($refererPage);
  }
}



Here is an example of how it works:
There is no need to change anything in the action files. Only the templates need to be slightly change.
Code: Select all
//Where usually we would just create a <div>, we use the create_div method
<?php yzDegradableAjax::create_div(array('id'=>'post_tags'));?>

<?php foreach($post->tags as $tag):?>
<li><?php echo link_to($tag->tag,'tag/show?id='.$tag->id); ?></li>
<?php endforeach;?>

<?php if($sf_user->isAuthenticated()): ?>
  <div>
  Add Your own tag:

  //The yzDegradableAjax works just the same as its default symfony counterpart, only difference lies in that you need to use it as a static method of yzDegradableAJAX.
  <?php echo yzDegradableAjax::form_remote_tag(array(
   'url'=>'tag/add',
   'update'=>'post_tags')); ?>
  <?php echo input_hidden_tag('post_id',$post->id);?>
  <?php echo input_auto_complete_tag('tag','','tag/autocomplete',array('autocomplete'=>'off'),'use_style=no'); ?>
  <?php echo submit_tag()?>
  </form>
  </div>
<?php endif; ?>
//The end tag of the div tag also needs to the end_create_div
<?php yzDegradableAjax::end_create_div('post_tags');?>


In the action function that usually handels the AJAX request, you only need to change the getRequestParameter():
In action.class.php of /app/module/tags/actions.php:
Code: Select all
...
  public function executeAdd()
  {
     //normally we use $this->getRequestParameter
// Only difference here is that use use yzDegradableAjax::getRequestParameter

$post_id = yzDegradableAjax::getRequestParameter('post_id');
     //$this->logMessage("PostID: in TagActions is: $post_id", 1);
     $user_id = $this->getUser()->getAttribute('subscriber_id','','subscriber');
     $phrase = yzDegradableAjax::getRequestParameter('tag');
     $this->post=sfDoctrine::getTable('Post')->find($post_id);
     $this->tags=PostTags::createNewTags($phrase, $user_id, $post_id);

  }
...

DrDrakken
Member
 
Posts: 80
Joined: Wed Jan 03, 2007 9:41 pm

Re: New Degradable AJAX Snipeet

by francois » Thu Feb 22, 2007 10:33 am

I'm not sure I understand fully your solution, but I think it is not the right way to build degradable Javascript.

First, take a look at here:

http://onlinetools.org/articles/unobtrusivejavascript/

The idea behind unobstrusive JavaScript is that the template should contain no 'onclick' nor 'onsubmit', because this is not unobstrusive. What should be done, is to output the XHTML structure without any javascript within, then run a JavaScript behaviour setter that parses the dom and defines the onclick and onsubmit properties.

This probably cannot be done with the existing JavaScript helpers, and probably doesn't require any helper at all, since the work to be done is mostly writing JavaScript...
francois
Faithful Member
 
Posts: 1638
Joined: Sat Oct 22, 2005 4:56 pm