Zend + jQuery Enhancements: Ajax Forms, Tooltip and Autofill

Some weeks ago Zend Framework 1.7 was released with the jQuery support I contributed. What is the essential advantage of using jQuery support in ZF? You can develop forms with ajax support either by using View Helpers or directly by the integrated Zend_Form support. The implementation of a DatePicker or AutoComplete functionality becomes as easy as using 2-3 lines of php code.

Currently only support for the jQuery UI library is shipped, but you can easily extend the jQuery support on your own and this blog post will show you how using three very popular jQuery plugins: AjaxForm, Tooltip and AutoFill. This will be a series of installments, beginning with the first one: AjaxForms

AjaxForm allows you to enhance any form of yours to submit the data with ajax to the server, so no additional overhead of loading a new page is necessary. Combining the power of Zend_Form and jQuery's ajaxForm you can even go so far as differentiating between successful and non-validated form submits. We will build a Form Decorator that integrates the AjaxForm plugin in any of your Zend_Form's. On submit it will send the form data to the server via ajax and clears the form afterwards. Clients that have javascript disabled will work too, the form is submitted to the server in a standard pre-ajax fashion and processed that way.

First what we need is obviously the new decorator, we will call it "My_JQuery_Form_Decorator_AjaxForm" and it will inherit from Zend_Form_Decorator_Form. What we then realize is, that this is just using a view helper to render, so what we need additionally is a "My_JQuery_View_Helper_AjaxForm" that extends from "Zend_View_Helper_Form". The code of the view helper will have to look as follows to fullfil our needs:

require_once "Zend/View/Helper/Form.php";class ZendX_JQuery_View_Helper_AjaxForm extends Zend_View_Helper_Form{  /**   * Contains reference to the jQuery view helper   *   * @var ZendX_JQuery_View_Helper_JQuery_Container   */  protected $jquery;  /**   * Set view and enable jQuery Core and UI libraries   *   * @param Zend_View_Interface $view   * @return ZendX_JQuery_View_Helper_Widget   */  public function setView(Zend_View_Interface $view)  {    parent::setView($view);    $this->jquery = $this->view->jQuery();    $this->jquery->enable()           ->uiEnable();    return $this;  }  public function ajaxForm($name, $attribs = null, $content = false, array $options=array())  {    $id = $name;    if(isset($attribs['id'])) {      $id = $attribs['id'];    }    if(!isset($options['clearForm'])) {      $options['clearForm'] = true;    }    if(count($options) > 0) {      require_once "Zend/Json.php";      $jsonOptions = Zend_Json::encode($options);      // Fix Callbacks if present      if(isset($options['beforeSubmit'])) {        $jsonOptions = str_replace('"beforeSubmit":"'.$options['beforeSubmit'].'"', '"beforeSubmit":'.$options['beforeSubmit'], $jsonOptions);      }      if(isset($options['success'])) {        $jsonOptions = str_replace('"success":"'.$options['success'].'"', '"success":'.$options['success'], $jsonOptions);      }    } else {      $jsonOptions = "{}";    }    $this->jquery->addOnLoad(sprintf(      '$("#%s").ajaxForm(%s)', $id, $jsonOptions    ));    return parent::form($name, $attribs, $content);  }}

It takes all the form-tag building of the inherited view helper for granted and just appends the necessary jQuery code to the jQuery onLoadActions stack. They will be outputted to the clients browser when calling <?php $this->jQuery(); ?> in your layout or view script. Make sure that you include the jQuery Form plugin in your code, for example with <?php $view->jQuery()->addJavascriptFile(..); >

Programming the decorator becomes a simple trick now:

require_once "Zend/Form/Decorator/Form.php";class My_JQuery_Form_Decorator_AjaxForm extends Zend_Form_Decorator_Form{  protected $_helper = "ajaxForm";  protected $_jQueryParams = array();  public function getOptions()  {    $options = parent::getOptions();    if(isset($options['jQueryParams'])) {      $this->_jQueryParams = $options['jQueryParams'];      unset($options['jQueryParams']);      unset($this->_options['jQueryParams']);    }    return $options;  }  /**   * Render a form   *   * Replaces $content entirely from currently set element.   *   * @param string $content   * @return string   */  public function render($content)  {    $form  = $this->getElement();    $view  = $form->getView();    if (null === $view) {      return $content;    }    $helper    = $this->getHelper();    $attribs    = $this->getOptions();    $name     = $form->getFullyQualifiedName();    $attribs['id'] = $form->getId();    return $view->$helper($name, $attribs, $content, $this->_jQueryParams);  }}

Now to use either the decorator for your form, or just the view helper to print your form tag with jQuery code you can invoke:

$form->addPrefixPath('My_JQuery_Form_Decorator', 'My/JQuery/Form/Decorator', 'decorator');$form->removeDecorator('Form')->addDecorator(array('AjaxForm', array(  'jQueryParams' => array(),)));$view->addHelperPath("My/JQuery/View/Helper", "My_JQuery_View_Helper");$view->ajaxForm("formId1", $attribs, $content, $options);

Now we finished up the view side of our script. Assuming that we use the Form Decorator instead of the View Helper, we can additionally add some fancy logic and error handling ajax fun to the action controller that is handling the Zend_Form instance.

class IndexController extends Zend_Controller_Action{  public function indexAction()  {    $foo = new MyAjaxTestForm();    try {      if(!$foo->isValid($_POST)) {        throw new Exception("Form is not valid!");      } else {        // do much saving and stuff here        if($this->getRequest()->isXmlHttpRequest()) {          $this->_helper->json(array("success" => "SUCCESSMESSAGEHERE"));        }      }    } catch(Exception $e) {      if($this->getRequest()->isXmlHttpRequest()) {        $jsonErrors = array();        foreach( ( new RecursiveIteratorIterator(new RecursiveArrayIterator($form->getMessages())) ) AS $error) {          $jsonErrors[] = $error;        }        $this->_helper->json->sendJson($jsonErrors);      }    }  }}

This has to be processed by a callback function of the AjaxForm and which may for example look like the following which uses a predefined div box (#formMessages, dont forget to implement it) to render either the success or the error messages.

$form->addDecorator(array('AjaxForm', array(  'jQueryParams' => array(    'success' => "formCallback1",   ),)));$view->jQuery()->addJavascript('function formCallback1(data) {  if(data.errors) {    $("#formMessages").append("<ul>");    for each(var item in data.errors) {      $("#formMessages").append("<li>"+item+"</li>");    }    $("#formMessages").append("</ul>");  } else {    $("#formMessages").html(data.success);  }}');

This seems very complex, but you could include that javascript code into the AjaxForm decorator and implement an Action Helper to do the action controller side of the stuff. This will be an exercise for a future post.

AutoFill and Tooltip extensions will be topic of the next installments of this series, so be aware of new content soonish.

Share This Post

  • Share on Twitter
  • Facebook
  • Share on deli.cio.us
  • Share on Digg
  • Share on reddit
  • Share on StumbleUpon

Comments


mezoni
Jan, 07. 2009

Make sure that you include the jQuery Form plugin in your code, for example with jQuery()-addJavascriptFile(..);

Why don't extend functiality by adding support for jQuery plugins?

You can add functions setLocalPluginPath, getLocalPluginPath?
This will help to do some operartions.

jQuery()-addJavascriptFile($view-jQuery()-getLocalPluginPath() . '/jquery.form.js');
?



mezoni
Jan, 07. 2009

You post an excellent example how to write Jquery_View_Helper.
But your method to convert callback options is not ideal.
I think you may to add function to do this into your helper like this.

protected function _convertOptionsToJson($options, $callbacks = array())
{
$callbackOptions = array();
$magic = '____';

if(count($options) 0) {
// Remove Callbacks if present
foreach($callbacks as $key) {
if(isset($options[$key])) {
$callbackOptions[$key] = $options[$key];
$options[$key] = $magic . $key;
}
}

$jsonOptions = Zend_Json::encode($options);

// Restore Callbacks if present
foreach($callbackOptions as $key = $value) {
$jsonOptions = str_replace('"' . $key . '":"' . $magic . $key . '"',
'"' . $key . '":' . $value, $jsonOptions);
}
} else {
$jsonOptions = "{}";
}

return $jsonOptions;
}



beberlei
Jan, 07. 2009

this is a genious solution. could you tell me what license this is under? i could put it into the abstract jQuery helper for use with all callback types, this would save tons of hazzle :-)



mezoni
Jan, 08. 2009

It's a free code snippet.
You can use this as you wish.
Thanks to you for your great work!



mezoni
Jan, 08. 2009

And don't forget to add 'jQuery js plugins' local path support into abstract jQuery helper.
setLocalPluginsPath() and getLocalPluginsPath() :-)
This will help to create independent from js-scripts locations custom view helpers.



mezoni
Jan, 09. 2009

Can you to refactore some code in your project?
It need to add fourth parameter if this is possible to functions with these arguments.
($id, $content, $params=array(), $attribs=array())
like this
($id, $content, $params=array(), $attribs=array(), callbacks=array())


callbacks, $callbacks);
// OR
// $this-callbacks = function_to_merge($this-callbacks, $callbacks)
// $params = _convertOptionsToJson($params, $this-callbacks);


$js = sprintf('%s("#%s").dialog(%s);',
ZendX_JQuery_View_Helper_JQuery::getJQueryHandler(),
$attribs['id'],
$params
);
$this-jquery-addOnLoad($js);

$html = '_htmlAttribs($attribs)
. ''
. $content
. '';
return $html;
}
}



Now _convertOptionsToJson() support array non-encoded parameters.
This need for some reasons.
Example:


dialogContainer('dlg1', $content, array('title' = 'Title'),
array(
'modal' = true,
'buttons' = array(
'Delete all items in recycle bin' = 'function(){ alert("deleted!"); }',
'Cancel' = 'function(){ alert("canceled!"); }'
)
)
) ?


Or more useful. What you think?

dialogContainer('dlg1', $content, array('title' = 'Title'),
array(
'modal' = true,
'buttons' = array(
'Delete all items in recycle bin' = ZendX_JQuery::lambda('alert("deleted")'),
'Cancel' = ZendX_JQuery::lambda('alert("canceled")')
)
)
) ?



public static function lamba() // or callback?
{
$args = func_get_args();

switch(func_num_args()) {
case 0:
return "function(){}";
case 1:
return sprintf("function(){ %s }", $args[0]);
default:
return sprintf("function(%s){ %s }", $args[0], $args[1]);
}
}



protected function _convertOptionsToJson($options, $callbacks = array())
{
$callbackOptions = array();
$magic = '____';

if(count($options) 0) {
// Remove Callbacks if present
foreach($callbacks as $key) {
if(isset($options[$key])) {
$callbackOptions[$key] = $options[$key];
$options[$key] = $magic . $key;
}
}

$jsonOptions = Zend_Json::encode($options);

// Restore Callbacks if present
foreach($callbackOptions as $key = $value) {
//
if(is_array($value)) {
$value = $this-_convertOptionsToJson($value, array_keys($value));
}

$jsonOptions = str_replace('"' . $key . '":"' . $magic . $key . '"',
'"' . $key . '":' . $value, $jsonOptions);
}
} else {
$jsonOptions = "{}";
}

return $jsonOptions;
}



stinie
Jun, 20. 2009

Hi Benjamin Eberlei,

Thanks for your great code. I try to understand everything and use it at work for a professional application.

I discovered a little inconsistency in your code.


class ZendX_JQuery_View_Helper_AjaxForm extends Zend_View_Helper_Form
{
....
}



Should be:


class My_JQuery_View_Helper_AjaxForm extends Zend_View_Helper_Form
{
....
}


I hope that you will post more blog articles like this. There is not a lot information on ZendX and Jquery out there. So I am very glad with your contribution.

Cheers, Christine (stinie)



koubel
Aug, 08. 2009

Nice example, thanks for impresive ZendX_Jquery work, I think that AjaxForm view helper can be part of ZendX_Jquery in the distribution.


Write a Comment