Ajaxify Symfony Admin Generator

This article has moved to this location.


Symfony admin generator is a great tool to simplify writing web interfaces. By simply typing php symfony propel:generate-admin backend Product you have an administration interface which a lot of features. You can filter, sort, paginate, create and edit an object without writing a single lines of code. All the features above can be customized through generator.yml. There are a variety of customizations available and you can read about those here.

In this article, i would like to show how to ajaxify symfony admin generator using jQuery.

Schema

The schema below is taken from Advanced-Forms tutorial.

<?xml version="1.0" encoding="UTF-8"?>
<database name="propel" package="lib.model.Product" defaultIdMethod="native">
  <table name="product">
    <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
    <column name="name" type="varchar" required="true" size="255" primaryString="true" />
    <column name="price" type="double" required="true" default="0" />
  </table>
  <table name="product_photo">
    <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
    <column name="product_id" type="integer" required="true" />
    <foreign-key foreignTable="product" onDelete="cascade">
      <reference local="product_id" foreign="id" />
    </foreign-key>
    <column name="filename" type="varchar" required="true" size="255" />
    <column name="caption" type="varchar" required="true" size="255" />
  </table>
</database>

Below is an example of the data used in this article

#data/fixtures/fixtures.yml
Product:
<?php for ($i = 1; $i <= 100; $i++): ?>
  product_<?php echo $i ?>:
    name:      Product <?php echo $i."\n" ?>
    price:     100
<?php endfor ?>

build all classes and load data:

php symfony propel:build-all-load

then initiate the product admin module

php symfony propel:generate-admin backend Product

Before we begin, copy the indexSuccess.php file in the generated cache directory into product module we’ve just created. If you haven’t load the jquery, open the indexSuccess.php and add the code below:

<?php use_javascript('/js/jquery-1.4.2.js') ?>

If we have a look carefully at the files generated by the generator, we can conclude that the file which play an important role in presenting the data is _list.php file, where the data is laid out inside sf_admin_list div tag. Later, the data within those tag will be replaced by result returned from AJAX request.

Pagination

Pagination is useful when we have large data. It is a bad practice to show all those data in a single page, instead you should split the data into more manageable chunks or ‘pages’. Fortunately, symfony already do it for you. We only need to customize it. For example, control the number of rows to be displayed in each page or ajaxify the pagination links.
How to ajaxify the pagination? If we have a look at generated _pagination.php file in the cache directory, you’ll notice a common pattern

<div class="sf_admin_pagination">
  <a href="<?php echo url_for('@product') ?>?page=1">
  ....
</div>

by using the pattern above, we can tell jQuery to ajaxify the links within the div.sf_admin_pagination tag and replace the data within sf_admin_list div tag by result returned from AJAX request.
Alright, first let us create a js file, say admin-list.js and put it into web/js directory. Add the piece of code below:

$(function() {
  var container = '.sf_admin_list';

  //start: handle pagination
  $(".sf_admin_pagination a").live('click',function(event) {
    hideContainer();
    $.get(this.href,{},function(response) {
      showContainer(response);
    });
    return false;
  });
  //end: handle pagination

  function hideContainer() {
    $(container).hide();
  };
  
  function showContainer(response) {
    $(container).html(response);
    $(container).show();
  };
});

Reopen the indexSuccess.php file and load the js file we have just created:

<?php use_javascript('/js/jquery-1.4.2.js') ?>
<?php use_javascript('/js/admin-list.js') ?>

Here i used the jquery live function. There’s pros and cons, you can find a good information about jquery live here.
In order to make our AJAX work, we have to override executeIndex function. In this function, first we have to see if the request is an AJAX request. If it’s false, just let the parent do the normal action.

public function executeIndex(sfWebRequest $request)
{
   parent::executeIndex($request);
   if ($request->isXmlHttpRequest()) {
      sfConfig::set('sf_web_debug', false);
      $this->setLayout(false);
      sfProjectConfiguration::getActive()->loadHelpers(array('I18N', 'Date'));
      return $this->renderPartial('product/list', array('pager' => $this->pager, 'sort' => $this->sort, 'helper' => $this->helper));
   } 
}

Alright, enough for pagination. Lets go to the next step..

Sorting

Sorting is most useful when the user can determine the sort order. A common idiom is to make the headers of sortable columns into links. As we have seen, jQuery allows us to eliminate such page refreshes by using AJAX methods. If we have the column headers set up as links as before, we can add jQuery code to change those links into AJAX requests.

Reopen then admin-list.js file, then add .sf_admin_list thead th[class*=sf_admin_list_th_] a to the jQuery selector, so it will become:

  //start: handle pagination and sorting
  $(".sf_admin_pagination a, .sf_admin_list thead th[class*=sf_admin_list_th_] a").live('click',function(event) {
    hideContainer();
    $.get(this.href,{},function(response) {
      showContainer(response);
    });
    return false;
  });
  //end: handle pagination and sorting

So far, we have successfully implemented the AJAX request for pagination and sorting. Next we will implement it for the form filter.

Form Filter

By default, In a list view, filters added automatically by symfony. With these filters, users can both display fewer results and get to the ones they want faster. You can add, remove which fields that will be displayed, or even remove it completely from a list view by configuring the filter section of generator.yml.

In the filter form, there are two actions that we have to ajaxify, submit button and Reset link.

1. Submit Button

Reopen the admin-list.js, add the code below after //end: handle pagination and sorting

  //start: handle form filter submission
  var formFilter = $('.sf_admin_filter form');
  formFilter.live('submit', function() {
    $.ajax({
      type: 'POST',
      dataType: 'text/html',
      url: formFilter.attr('action'),
      data: formFilter.serialize(),
      beforeSend: function(){ hideContainer(); },
      success: function(response) { showContainer(response); }
    });
    return false;
  });
  //end: handle form filter submission

When the user click a submit button, the executeFilter function will be executed.
Open the actions class, copy public function executeFilter() from generated actions class into it and replace

  $this->redirect('@product');

by

   if ($request->isXmlHttpRequest()) {
      return $this->executeIndex($request);
   } else {
      $this->redirect('@product');
   }

so it’ll become like below:

public function executeFilter(sfWebRequest $request)
{
   $this->setPage(1);
   if ($request->hasParameter('_reset'))
   {
     $this->setFilters($this->configuration->getFilterDefaults());
     if ($request->isXmlHttpRequest()) {
        return $this->executeIndex($request);
     } else {
        $this->redirect('@product');
     }
   }
   $this->filters = $this->configuration->getFilterForm($this->getFilters());
   $this->filters->bind($request->getParameter($this->filters->getName()));
   if ($this->filters->isValid())
   {
     $this->setFilters($this->filters->getValues());
     if ($request->isXmlHttpRequest()) {
        return $this->executeIndex($request);
     } else {
        $this->redirect('@product');
     }
   }
   $this->pager = $this->getPager();
   $this->sort = $this->getSort();
   $this->setTemplate('index');
}

2. Reset Link

In order to ajaxify reset link, first copy _filters.php from generated cache into product templates, then remove the ‘method’ => ‘post’ parameter passed to reset link creation (I still haven’t found the proper way how to ajaxify this link without removing the post method parameter).

/*before*/
<?php //echo link_to(__('Reset', array(), 'sf_admin'), 'product_collection', array('action' => 'filter'), array('query_string' => '_reset', 'method' => 'post')) ?>

/*after*/
<?php echo link_to(__('Reset', array(), 'sf_admin'), 'product_collection', array('action' => 'filter'), array('query_string' => '_reset')) ?>

Add the code below to the admin-list.js after //end: handle form filter submission

  //handle filter reset
  $('.sf_admin_filter tfoot td a').live('click', function(event) {
    hideContainer();
    $.get(this.href,{},function(response) {
      formFilter[0].reset();
      showContainer(response);
    });
  });

Here are the full source code for admin-list.jst with a few refactoring

$(function() {
  var container = $('.sf_admin_list');
  var formFilter = $('.sf_admin_filter form');

  //start: handle pagination and sorting
  $(".sf_admin_pagination a, .sf_admin_list thead th[class*=sf_admin_list_th_] a").live('click',function(event) {
    return getAjaxResponse('GET', this.href, {}, false);
  });
  //end: handle pagination

  //start: handle form filter submission
  formFilter.live('submit', function() {
    return getAjaxResponse('POST', formFilter.attr('action'), formFilter.serialize(), false);
  });
  //end: handle form filter submission

 //start: handle reset filter
  $('.sf_admin_filter tfoot td a').live('click', function(event) {
    return getAjaxResponse('GET', this.href, {}, true);
  });
  //end: handle reset filter

  function getAjaxResponse(type, url, data, reset_filter) {
    $.ajax({
      type:type,
      dataType:'text/html',
      url: url,
      data:data,
      beforeSend:function() { hideContainer(); },
      success:function(response) {
        if (reset_filter == true) formFilter[0].reset();
        showContainer(response);
      }
    });
    return false;
  };

  function hideContainer() {
    container.hide();
  };

  function showContainer(response) {
    container.html(response);
    container.show();
  };
});

If you have ideas to make it better, please let me know, and i will be glad.

Update 2
I’ve packaged it as a sfAdminAjaxThemePlugin and hosted in the symfony plugin repository. It’s comes with two themes, ajaxtheme for propel 1.4 and ajaxtheme15 for propel 1.5.

Update 1
If you want to ajaxify delete links, you can override linkToDelete function

  //product/lib/productGeneratorHelper.class.php
  public function linkToDelete($object, $params)
  {
    $link = parent::linkToDelete($object, $params);
    if ($link != '') {
      $link = str_replace('f.submit()', 'getAjaxResponse(\'POST\', this.href, $(f).serialize())', $link);
    }
    return $link;
  }

and override executeDelete function

public function executeDelete(sfWebRequest $request)
{
  $request->checkCSRFProtection();
  $this->dispatcher->notify(new sfEvent($this, 'admin.delete_object', array('object' => $this->getRoute()->getObject())));
  $this->getRoute()->getObject()->delete();

  if ($request->isXmlHttpRequest()) {
    return $this->executeIndex($request);
  } else {
    $this->getUser()->setFlash('notice', 'The item was deleted successfully.');
    $this->redirect('@product');
  }
}

below is my modified admin-list.js (You have to include jquery blockUI in order to get it work, see comment from @jp_morvan)

function startLoader() {
  $.blockUI({
    message: "<h1> Update in progress... </ h1> <h2> Please wait. </ h2>",
    css: {
      border: 'none',
      padding: '15px',
      backgroundColor: '#000',
      '-webkit-border-radius': '10px',
      '-moz-border-radius': '10px',
      opacity: .9,
      color: '#fff'
     }
   });
}

function hideLoader() { $.unblockUI(); }

function getAjaxResponse(type, url, data, flt) {
  $.ajax({
    type:type,
    dataType:'text/html',
    url: url,
    data:data,
    beforeSend:function() {startLoader();},
    success:function(response) {
      if (flt != undefined && flt.length > 0) {
        flt[0].reset();
      }
      jQuery('#sf_admin_content .sf_admin_list').html(response);
      hideLoader();
    }
  });
  return false;
}

$(function() {
  var formFilter = $('#sf_admin_bar form');

  $('.sf_admin_pagination a, .sf_admin_list thead th[class*=sf_admin_list_th_] a').live('click', function(e) {
    return getAjaxResponse('GET', this.href, {});
  });

  $('#sf_admin_bar form input[type=submit]').live('click', function(e) {
    return getAjaxResponse('POST', formFilter.attr('action'), formFilter.serialize());
  });

  $('#sf_admin_bar form tfoot td a').live('click', function(e) {
    return getAjaxResponse('GET', this.href, {}, formFilter);
  });
});

Hope it useful.. thanks you and have fun with symfony!!!

19 responses to “Ajaxify Symfony Admin Generator

  1. Damian November 22, 2010 at 12:58 pm

    Is there a demo? 🙂

  2. jp_morvan November 22, 2010 at 3:16 pm

    Hi !

    It seems to be really great ! I hope I can try this soon !

  3. jp_morvan November 22, 2010 at 9:37 pm

    Ok,
    I tried and It works well.
    I’ve replaced hide() and show() methods by a $.blockUI()
    (plugin here http://jquery.malsup.com/block/ ).
    My update : http://sacricri.free.fr/php/admin-list.html

    Regards

  4. Pingback: sfAdminAjaxThemePlugin now in symfony plugin repository « Just Blog, No More…

  5. Serj December 28, 2010 at 12:52 am

    Hi
    Thank you for your plugin!
    Just installed it but got error: Fatal error: Cannot instantiate abstract class BaseMltestGeneratorConfiguration
    Mltest is my module name

    • nibsirahsieu December 28, 2010 at 6:19 am

      Thanks you for your feedback.. i’ve updated my plugin..

      • Serj December 28, 2010 at 10:35 pm

        Thanks for fast response.
        Now i’m getting:
        Fatal error: Cannot redeclare class BaseMymodelPeer
        Seems ajax this class is being loaded twice after call of executeIndex method in action.

      • nibsirahsieu December 29, 2010 at 5:45 am

        i can’t produce an error like that. i’ve tested it with sfPropelPlugin and sfPropel15Plugin, its work well. Try to clear the cache, remove and recreate the admin module

      • Serj December 29, 2010 at 6:37 pm

        Forget to tell. It happens when i’m tring to sort column in list mode.
        i’m using sfPropelPlugin

      • nibsirahsieu December 29, 2010 at 8:03 pm

        i still can’t produce an error. Check your Peer class, if it is contain include require statement, change it to require_once, or disable the require statements through propel.ini file by adding propel.addIncludes = false then rebuild your model

  6. Serj January 4, 2011 at 9:12 pm

    Thanks, that was my problem

  7. Hans May 9, 2011 at 10:58 pm

    Good striaghtforward stuff. Pity you use Propel and not Doctrine 😉

    Quick fix. where you have “Here are the full source code for admin-list.jst with a few refactoring” you missed a couple $s:


    function hideContainer() {
    container.hide();
    };

    function showContainer(response) {
    container.html(response);
    container.show();
    };
    });

    should be:

    function hideContainer() {
    $container.hide();
    };

    function showContainer(response) {
    $container.html(response);
    $container.show();
    };
    });

  8. article directory list January 14, 2012 at 5:56 am

    Ola! Nibsirahsieu,
    Thanks for the above, Many people do not know that but there is another option now available for people to take loans and solve the problem and that loan option is that of the title loans.
    Catch you again soon!

Leave a reply to nibsirahsieu Cancel reply