Login

Drupal 7 Multi-Step Form (FAPI) with Table Report

In this post we are going to show how easy it is using Drupal 7's Form API (aka FAPI) to create a multi-step form with a simple html table report.

We'll also demonstrate the development technique of stubbing. In our case, we haven't decided how we want to store our data (for our final design). Therefore, we will use a simple Collection class and Drupal cache mechanism to perform a temporary yet simple stub.

The stub allows a developer to concentrate on one task (e.g. form/user interface), while another developer designs the full implementation (i.e. decides/codes a Drupal Field API instance, custom database tables, etc).

In our example we going to have users enter their name and a favorite color. The first step in our multi-step form is to ask the user to enter their name:

The user enters their name and then clicks on "Next Step". If the use clicks on "Reset",  the first and last names fields are cleared and our form is re-displayed in it's initial state.  The following screen shows a user entering their first and last name:

The user now clicks on "Next Step" and we get the following screenshot:

If the user presses "Previous Step" we re-display the first and last name (e.g. Joe Smoe).  The idea is, let the user edit or clear their entry. Don't force them to re-enter their name.  If the user clicks on "Submit - Complete Forn", we are going to add their entry (e.g. first name, last name and favorite color) both to our cache storage and to our html report. 

The following screens shows what happens when Joe Smoe chooses "red" and clicks on "Submit - Complete Form":

So lets look at our source code. We break up the main form definition so that you can get an idea of how the "steps" are processed. We use a separate method to define the form controls for step 1 and step 2.

function multi_step_form($form, &$form_state ) {
 
  if (empty($form_state['storage']['step'])) {
        $form_state['storage']['step'] = 0;
  }
 
  switch($form_state['storage']['step']) {
    case 0:
      $form=  _handler_step_0($form, $form_state);
    break;
    case 1:
      $form =  _handler_step_1($form, $form_state);
    break;
  }
 
  // This is where we render our report
  $folks = getCachedFolks();
  $form['report'] = array(
    '#type' => 'markup',
    '#markup' => $folks->render_table(),
    '#weight' => 10,
  );
  return $form;
}
 
function _handler_step_0($form, &$form_state) {
 
  $weight = 0;
  $form['first_name'] = array(
      '#type' => 'textfield',
      '#size' => 10,
      '#maxlength' => 10,
      '#title' => t('First Name'),
      '#weight'=>$weight++,
      '#prefix' => '<table border=0 cellspacing=3 celladding=3><tr><td>',
      '#suffix' => '</td>',
      '#default_value' => isset($form_state['storage']['first_name']) ? $form_state['storage']['first_name'] : '',
   );
 
   $form['last_name'] = array(
      '#type' => 'textfield',
      '#size' => 10,
      '#maxlength' => 10,
      '#title' => t('Last Name'),
      '#weight' => $weight++,
      '#prefix' => '<td>',
      '#suffix' => '</td>',
      '#default_value' => isset($form_state['storage']['last_name']) ? $form_state['storage']['last_name'] : '',
  );
 
   $form['reset'] = array('#type' => 'submit', '#value' => 'Reset',  '#weight' => $weight++,
      '#prefix' => '<td>',
      '#suffix' => '</td>',
      );
 
  $form['submit'] = array('#type' => 'submit', '#value' => 'Next Step',  '#weight' => $weight++,
      '#prefix' => '<td>',
      '#suffix' => '</td></tr></table>',
      );
 
  return $form;
 
}
 
function _handler_step_1($form, &$form_state) {
 
  $weight = 0;
  $form['color'] = array(
      '#type' => 'select',
      '#options' => array('red' => 'red', 'white' => 'white', 'blue' => 'blue'),
      '#default_value' =>'red',
      '#title' => t('Favorite Color'),
      '#weight'=>$weight++,
      '#prefix' => '<table border=0 cellspacing=2 celladding=2><tr><td>',
      '#suffix' => '</td>',
   );
 
  $form['submit'] = array('#type' => 'submit', '#value' => 'Submit - Complete Form',  '#weight' => $weight++,
      '#prefix' => '<td>',
      '#suffix' => '</td>',
      '#title' => t('Click this button to complete the form and submit your entries.'),
      );
 
  $form['cancel'] = array('#type'=>'submit', '#weight'=>$weight++, '#value'=>'Previous Step',
      '#prefix' => '<td>',
      '#suffix' => '</td></tr></table>',
      );
 
  return $form;
}

Note! Since this post demonstrates a module in early development, we simply added the html table markup to the form definition itself. Certainly it's better the implement a theme() function to theme your form.

Also Note! For sake of brevity I omitted a validation handler.

Let's look our submit handler. It's fairly simple, we look at which step we are on and which button was clicked. We use the $form_state['storage'] structure to store the user's entry in case they want to toggle the next-step/previous step buttons.

 
function multi_step_form_submit($form, &$form_state) {
 
  switch ($form_state['storage']['step']) {
 
        case 0:
          switch($form_state['values']['op']) {
            case 'Next Step' :
              $form_state['rebuild'] = TRUE;
              $form_state['storage']['first_name'] = $form_state['values']['first_name'];
              $form_state['storage']['last_name'] = $form_state['values']['last_name'];
              $form_state['storage']['step'] = 1;
            break;
            case 'Reset' :
                unset($form_state['storage']);
                drupal_goto('test/multi_step_form');
              break;
          }
        break;
 
        case 1:
            switch($form_state['values']['op']) {
              case 'Previous Step':
                $form_state['storage']['step'] = 0;
                $form_state['rebuild'] = TRUE;
              break;
              case 'Submit - Complete Form' :
                $folks = getCachedFolks();
                $folks->add(folk::create()
                       ->setFirst_name($form_state['storage']['first_name'])
                       ->setLast_name($form_state['storage']['last_name'])
                       ->setColor($form_state['values']['color']));
                cache_set('FOLKS_CACHE_ID', serialize($folks), 'cache');
                drupal_goto('test/multi_step_form');
             break;
          }
        break;
    }
}

Next, let's look at our stub code. We have a couple generic classes that we re-use for other projects (e.g. keyed-entity, a Collection).

In many of our setter methods we return $this, so that we can chain the method calls.

interface i_keyed_entity {     
  public function getName();
  public function setName($name);
  public function getIndex();
  public function setIndex($index);
}
 
class keyed_entity implements i_keyed_entity {
 
  protected $name;
  protected $index;
 
  public function getName() {
    return $this->name;
  }
 
  public function setName($name) {
    $this->name = $name;
  }
 
  public function getIndex() {
    return $this->index;
  }
 
  public function setIndex($index) {
    $this->index = $index;
  }
}
 
class folk extends keyed_entity {
 
      protected $first_name;
      protected $last_name;
      protected $color;
 
      public static function create() {
         return new folk();
      }
 
      public function getFirst_name() {
        return $this->first_name;
      }
 
      public function setFirst_name($first_name) {
        $this->first_name = $first_name;
        return $this;
      }
 
      public function getLast_name() {
        return $this->last_name;
      }
 
      public function setLast_name($last_name) {
        $this->last_name = $last_name;
        return $this;
      }
 
      public function getColor() {
        return $this->color;
      }
 
      public function setColor($color) {
        $this->color = $color;
        return $this;
      }
 
      public function toArray() {
          return array('first_name' => $this->first_name,
                      'last_name' => $this->last_name,
                      'color' => $this->color,);
      }
 
}
 
class Collection {
 
  protected  $list;
 
  public function __construct() {
    $this->list = array();
  }
 
  public function add(i_keyed_entity $element) {
      $index = count($this->list);
      if ($element->getIndex() == NULL) {
        $element->setIndex($index);
      }
      $this->list[] = $element;
      return $this;
  }
 
  public function get($index) {
    return $this->list[$index];
  }
 
  public function getIndex($name) {
    $index = 0;
    foreach($this->list as $element) {
      if ($name == $element->getName()) {
        $index = $element->getIndex();
        break;
      }
    }
    return $index;
  }
 
  public function getList() {
    return $this->list;
  }
 
  public function setList($list) {
    $this->list = $list;
  }
 
  public function toOptionArray($extra_element = NULL) {
    $options = array();
    foreach ($this->list as $element) {
      $options[$element->getIndex()] = $element->getName();
    }
 
    if (isset($extra_element)) {
      $index = count($this->list);
      $options[$index] = $extra_element;
    }
    return $options;
  }
 
  public function getLastKey() {
    $size = count($this->list);
    return $size;
  }
 
}
 
class folks_collection {
 
    private static $instance;
    protected $folks;
 
    private function __construct() {
      $this->folks = new Collection();
    }
 
 
    public static function instance() {
      if (self::$instance === NULL) {
          self::$instance = new folks_collection();
      }
      return self::$instance;
    }
 
    public function add(i_keyed_entity $element) {
      $this->folks->add($element);
      return $this;
    }
 
    public function get($index) {
      return $this->folks->get($index);
    }
 
    public function getList() {
      return $this->folks->getList();
    }
 
    public function render_table() {
      $vars['header'] = array('First Name','Last Name', 'Color');
      $vars['empty'] = 'Empty List - No Entries';
      $vars['rows'] = array();
 
      foreach ($this->folks->getList() as $folk) {
          $vars['rows'][] = $folk->toArray();
      }
 
      return theme('table',$vars);
    }
}

Note! The folk_collection uses a Singleton pattern. We only want to have one instance for any user request.

Finally, let's look at our cache routine. Normally, we would create a separate cache table for our custom module, but again for sake of brevity I just re-use the base Drupal "cache" table.

 
function getCachedFolks($reset = FALSE) {
  static $cached_data;
  if (!isset($cached_data) || $reset) {
    if (!$reset
            && ($cache = cache_get('FOLKS_CACHE_ID','cache'))
            && !empty($cache->data)) {
      $cached_data  = unserialize($cache->data);
    }
    else {
       $folks = folks_collection::instance();
       // ->add(folk::create()->setFirst_name('Lorin')->setLast_name('Klugman')->setColor('red'));
      $cached_data = $folks;
      cache_set('FOLKS_CACHE_ID', serialize($cached_data), 'cache');
    }
  }
  return $cached_data;
}

 

Comments

How do I clear the entries from the database

Hi,
Nice example. I tried it. Was able to enter and then get my system to display multiple entries in the report. Do I have to clear the cache to delete the records?

Yes - Clear the cache to delete the entries

Hi,

For brevity sake I didn't implement an explicit "delete entries" method. You are exactly correct, you clear the cache (i.e. log in as an admin, go to configuration, then performance, the clear cache).

Again, we aren't recommending storing you normal data in cache. We just wanted to show you how you might stub it on a development machine (installation). Note@ We never develop on remote hosts. we always string test our code using a debugger before deploying anything to a QA environment (let alone a remote production installation).

Hope that helps.

Jason