ybits (why bits? because.)

Every day I become more of a jQuery fan, and every day I find some other jQuery plugin that does something I need to do. Today, that something is table sorting. There is, of course, a jQuery plugin for this: Tablesorter. It’s simple. I like simple.

Most Javascript-based table sorting solutions lack persistence, as they probably should, and Tablesorter is no exception. There’s a common belief that all initial sorting should be done on the back-end, and that using Javascript to do it is bad practice. To those who harbor this belief, I say: Whatever! What I want is a drop-in solution to use with Tablesorter, which is itself a drop-in solution. I’m all about dropping things in, and don’t steal my thunder with your “ideals.”

I can hear you say: “Why not just use Ajax or pass some query parameters to your script that modify the ‘order by’ section of your SQL statement?” Good question. Or is it? (Hint: no)

I won’t provide a long, convoluted explanation, but the simplest way to look at it is this: Data can be built in different places and in different ways. Maybe you can quickly write something that handles the first part of that, but how about the second? What if the data isn’t being built directly from a SQL statement. What if the columns you see (in the HTML table) don’t map to columns in a database? And so on. Work work work.

This is getting complicated. Let’s simplify. What did I say I wanted to do? Ah yes, persistently sort HTML tables. And that’s it. I don’t care where the data is coming from, I don’t care what it’s for, or what scripting language is used to create it. I just want to sort it, and have the sort stick.

And I’m willing to use cookies to do it. Let’s have a look at how, together.

Tablesorter has a widget interface and a parser interface. “Persistent sorting” doesn’t really sound like a widget or a parser, but it sounds less like a parser so let’s make a widget. To do this, we’ll also need the jQuery JSON plugin and the jQuery Cookie plugin.

Once we have all that, we can make a widget that creates a cookie to keep a map of the last sort done to a table, using the form: {tableId: sortList}. The idea is to set the cookie sortList whenever a sort is done on a table, and check the cookie for an existing sortList every time a page with the table loads. If a sortList exists for the table, sort with it, which will also then save it to the cookie, and so on.

Lucky for us, Tablesorter’s widget interface fires off the function format(table) every time the sorter is initialized, and once after every sort, which is exactly what we need. The code:

$(document).ready(function() {
  $.tablesorter.addWidget({
    // give the widget an id
    id: "sortPersist",
    // format is called when the on init and when a sorting has finished
    format: function(table) {

      // Cookie info
      var cookieName = 'MY_SORT_COOKIE';
      var cookie = $.cookie(cookieName);
      var options = {path: '/'};

      var data = {};
      var sortList = table.config.sortList;
      var tableId = $(table).attr('id');
      var cookieExists = (typeof(cookie) != "undefined" && cookie != null);

      // If the existing sortList isn't empty, set it into the cookie and get out
      if (sortList.length > 0) {
        if (cookieExists) {
          data = $.evalJSON(cookie);
        }
        data[tableId] = sortList;
        $.cookie(cookieName, $.toJSON(data), options);
      }

      // Otherwise...
      else {
        if (cookieExists) { 

          // Get the cookie data
          var data = $.evalJSON($.cookie(cookieName));

          // If it exists
          if (typeof(data[tableId]) != "undefined" && data[tableId] != null) {

            // Get the list
            sortList = data[tableId];

            // And finally, if the list is NOT empty, trigger the sort with the new list
            if (sortList.length > 0) {
              $(table).trigger("sorton", [sortList]);
            }
          }
        }
      }
    }
  });
  $("#maintable").tablesorter({widgets: ['sortPersist']});
});

And there you have it, a Tablesorter widget that implements a persistable sort using cookies. Here’s an example of it in action. Sort on a column, then refresh to page to see that the column is still being sorted.

September 23rd, 2009

Zend_Db_Table and Zend_Db_Table_Row provide a great base for the “model” portion of the MVC paradigm implemented by the Zend Framework. Zend_Db_Table abstracts an underlying database table, and Zend_Db_Table_Row abstracts a row. They provide normalized functionality, are database agnostic, and offer other types of slickness that won’t be covered here.

If you haven’t experimented with Zend_Db yet, here’s a simple example of the basic abstractions:

Insert a record:

if ($this->_request->isPost()) {
    $usersModel = new Users();
    $user = $usersModel->createRow();
    $user->name = $this->_request->getParam('name');
    $user->age = $this->_request->getParam('age');
    $user->save();
}

Update a record:

$id = (int)$this->_request->getParam('id');
$usersModel = new Users();
$user = $usersModel->getRow('id = ?', $id);
if (!empty($user)) {
    // Just set the name to uppercase..
    $user->name = strtoupper($user->name);
    $user->save();
}

Notice that when $row->save() is called, the appropriate insert/update is made depending upon whether the row is new or not.

The update method is even smart enough to only update fields that have been marked as modified.

But there is a small problem. Let’s have a look at Zend_Db_Table_Row_Abstract::_set() to see what happens when a value is set into a row:

public function __set($columnName, $value)
{
    $columnName = $this->_transformColumn($columnName);
    if (!array_key_exists($columnName, $this->_data)) {
        require_once 'Zend/Db/Table/Row/Exception.php';
	throw new Zend_Db_Table_Row_Exception("Specified column \"$columnName\" is not in the row");
    }
    $this->_data[$columnName] = $value;
    $this->_modifiedFields[$columnName] = true;
}

Observe at the marked lines that the column is marked modified regardless of the value being set. If the value passed in is already set for that column, it’s still marked modified even though technically it hasn’t been.

This may or may not be desirable given the context, but consider this: Data is always posted in a form, whether or not it changes on the client side. It doesn’t make sense to always compare each field with the column in the row to determine if the data has changed. Surely this can be done better.

What makes sense is for the row to do this check automatically. What makes even more sense is to make the modified columns and their values available, so that controller specific logic can be written to handle special cases when certain data has changed.

It all starts with that Zend_Db_Table_Row_Abstract::_set() function.

What we want is to make the _set function smarter, so that setting a value into a column that already has that value doesn’t also mark it modified.

Zend_Db_Table_Row_Abstract can be extended to add the functionality, then Zend_Db_Table_Abstract can be extended to use the new extended row class. When the model is defined, it will extend My_Db_Table_Abstract and inherit the new functionality.

An overloaded Zend_Db_Table_Row_Abstract:

class My_Db_Table_Row extends Zend_Db_Table_Row_Abstract
{
    // Return modifiedFields
    public function getModifiedFields()
    {
    	return $this->_modifiedFields;
    }

    // Return a hash of only modified field => value pairs
    public function getModifiedValues()
    {
    	$modifiedValues = array();
    	foreach ($this->_modifiedFields as $field => $value)
    	{
    	    $modifiedValues[$field] = $this->_data[$field];
    	}
    	return $modifiedValues;
    }

    //Override, don't allow unmodified fields to be marked...
    public function __set($columnName, $value)
    {
        $columnName = $this->_transformColumn($columnName);
        if (!array_key_exists($columnName, $this->_data)) {
            require_once 'Zend/Db/Table/Row/Exception.php';
            throw new Zend_Db_Table_Row_Exception("Specified column \"$columnName\" is not in the row");
        }

        $origData = $this->_data[$columnName];
        if ($origData != $value) {
            $this->_data[$columnName] = $value;
            $this->_modifiedFields[$columnName] = true;
        }

    }
}

An overloaded Zend_Db_Table_Abstract:

class My_Db_Table_Abstract extends Zend_Db_Table_Abstract {

    protected $_rowClass = 'My_Db_Table_Row';

    public function columnExists($column)
    {
        return in_array($column, $this->_cols);
    }
}

Portion of a controller script utilizing the new row:

$form = new MyForm();
$form->populate($this->_request()->getPost();
$id = (int)$this->_request->getParam('id');
$usersModel = new Users();
$user = $usersModel->getRow('id = ?', $id);
if (!empty($user)) {
    $user->name = $form->getElement['name']->getValue();
    $user->age = $form->getElement['age']->getValue();
    $mods = user->getModifiedValues();
    $user->save();

    foreach($mods as $mod) {
	// do something with the modified values;
    }
}

September 23rd, 2009

A Zend_Form is (or at least, can be) an unfortunate mish-mash of logical and visual rules. Instead of designing how the form will render visually using HTML in a .phtml file, like everything else that’s rendered, there are a bunch of programmatic hoops to jump through to set up the visual properties in code using decorators.

The hoops aren’t so bad if the layout of the form is simple, for example a basic layout with all the labels to the left and all the elements on the right. But to create a complex form layout using tables (yes, we still use tables for tabular data), the table layout must be created in HTML anyway, and then back-fitted into code by breaking the structure apart and assigning different pieces to individual elements of the form.

This process is tedious, and twice the amount of work to get the same end result. Plus, to change something visually, either the template has to be modified until it’s right and then back-fitted again, or the visual properties have to be tweaked directly in code without the benefit of the template. The latter being the “hope for the best” method.

Mashing this visual structure into the form object itself:

  • Encroaches on the role of the view.
  • Breaks separation of concerns.
  • Is harder than it should be.

The hybrid approach to Zend_Form is to use all of its functionality except the decorators for visual layout. Instead, the form is laid out normally in a view script, and each element of the form is rendered individually where it needs to be.

This process provides the best of both worlds. The form object is still used to define the elements and do validations, but the actual layout is done (once) in HTML, which is easier to create, modify and maintain.

Here’s an example, with bits of a form object, controller, and view script:

MyForm.php:

class MyForm extends Zend_Form
{
    public function __construct($options = null)
    {
        parent::__construct($options);
        $this->setName('my_form');

        $textFilters = array(
            'StripTags',
            'StringTrim'
        );

        $options = array(
            'name' => 'name',
            'required' => true,
            'validators' => array('NotEmpty'),
            'filters' => $textFilters,
            'class' => 'form',
            'size' => 65,
            'required' => true
        ); 

        $input = new Zend_Form_Element_Text($options);
        $this->addElement($input);

        $options = array(
            'name' => 'age',
            'required' => false,
            'validators' => array('NotEmpty'),
            'filters' => $textFilters,
            'class' => 'form',
            'size' => 5,
            'required' => true
        ); 

        $input = new Zend_Form_Element_Text($options);
        $this->addElement($input);

        $element = new Zend_Form_Element_Submit('submitbtn');
        $element->setAttrib('id', 'submitbtn')
                      ->setAttrib('class', 'button')
                      ->setLabel('Save');
        $this->addElement($element);

        $this->setElementDecorators(array(
            'ViewHelper',
            'Errors'
         ));
    }
}

MyController.php:

public function showAction()
{
    $form = new MyForm();
    $form->populate($this->_request->getPost());
    $this->view->form = $form;
}

show.phtml:

<form
name="<?php echo $this->form->getName() ?>"
id="<?php echo $this->form->getName() ?>"
method="post" enctype="application/x-www-form-urlencoded"
action=""
>
<table>
<tr>
<td><?php echo $this->form->getElement('name')->getLabel() ?></td>
<td><?php echo $this->form->getElement('name')?></td>
</tr>

<tr>
<td><?php echo $this->form->getElement('age')->getLabel() ?></td>
<td><?php echo $this->form->getElement('age')?></td>
</tr>

<tr>
<td></td>
<td><?php echo $this->form->getElement('submitbtn')?></td>
</tr>

</table>
</form>