Monday, June 18, 2012

Drupal 7 Webform with an item catalog table element

I was working on a Drupal 7 project and there was a requirement of a form with some simple text fields to put values and some elements in a grid like format to display catalog of items including item name, price, a text box for quantity and automatically calculated sub totals and grand total.



I started with webform module but the built in grid element did not work. Then I tried webform_table_element module, it had some bugs but I managed to configure it. But, unfortunately, it was not able to produce a similar form. I was on a tight deadline (as usual) and did not have time to do something custom, so I decided to try some workaround and after spending a little time I got acceptable result.

I am writing some simple steps to re-generate similar result. I know this is not an ideal solution but it can save a little time if you have to add a couple of similar forms in your Drupal based project.


You need to install webform and webform_table_element module modules first.

a) Add a field of type table_element in webform. table_element type requires you to put key|value pairs in 'Rows' textarea there, convert your 'value' to a simple JSON so it can be accessed later, something like:

1|{"ID":"001", "title":"My Item 1", "rate":"245"}
2|{"ID":"002", "title":"My Item 2", "rate":"460"}
3|{"ID":"003", "title":"My Item 3", "rate":"740"}

And here is a screenshot:

b) Add these child elements under newly created table_element type item:


  1.  Item Name
    1. Type: markup
    2. Label: Item Name
    3. Field Key: item_name
    4. Value: Name
  2. Item Rate
    1. Type: markup
    2. Label: Item Rate
    3. Field Key: item_rate
    4. Value: Rate
  3. Quantity
    1. Type: textfield
    2. Label: Quantity
    3. Field Key: quantity
  4. Item Rate
    1. Type: markup
    2. Label: Sub Total
    3. Field Key: sub_total
    4. Value: <div class="sub-total"></div>
      (you will need to disable rich text editor or write in source view with Text Format=Full HTML)

c) We are done with form building here, now time to add some code in template.php file:

// This will display form element in a multi column table

function adibf_table_element($variables) {
  $element = $variables['element'];
  $header = array();
  $header_complete = false;
  $rows = array();
  
  foreach (element_children($element) as $child) {
    $child_element = $element[$child];
   
 $rowCustomData = $child_element['#row_title'];
 $rowCustomData = json_decode($rowCustomData);
   
    $row = array($rowCustomData->ID);
 
    foreach (element_children($child_element) as $grandchild) {
      
      if (!$header_complete && isset($element['#row_titles'])) {
       if($child_element[$grandchild]['#type'] == 'markup') {
   $theKey = trim(strip_tags($child_element[$grandchild]['#markup']));
   $theTitle = '';
   
   switch($theKey) {
    case 'Name':
     $theTitle = t('Item');
     break;
    
    case 'Rate':
     $theTitle = t('Rate (usd)');
     break;
    
    case 'Quantity':
     $theTitle = t('Quantity');
     break;
   }
   
   if($element['#column_titles'][$grandchild] == 'sub-total')
    $theTitle = t('Sub Total');
   
   $header[]  = array('data' => $theTitle, 'class' => $element['#column_titles'][$grandchild]);
  } else {
         $header[]  = array('data' => t($element['#row_titles'][$grandchild]), 'class' => $element['#column_titles'][$grandchild]);
  }
      }
  
  if($child_element[$grandchild]['#type'] == 'markup') {
   if(trim(strip_tags($child_element[$grandchild]['#markup'])) == 'Name')
    $child_element[$grandchild]['#markup'] = $rowCustomData->title;
  
   if(trim(strip_tags($child_element[$grandchild]['#markup'])) == 'Rate')
   $child_element[$grandchild]['#markup'] = $rowCustomData->rate;
  
   if(trim(strip_tags($child_element[$grandchild]['#markup'])) == 'Color')
   $child_element[$grandchild]['#markup'] = $rowCustomData->color;
  
   if(trim(strip_tags($child_element[$grandchild]['#markup'])) == 'Dimension')
   $child_element[$grandchild]['#markup'] = $rowCustomData->dimension;
  }
  
     $row[] = array('data' => drupal_render($child_element[$grandchild]));
    }

    $header_complete = true;
    $rows[] = $row;
  }

  array_unshift($header, array('class' => 'row-title', 'data' => t('Code')));
  return theme('table', array('header' => $header, 'rows' => $rows));
}

// This will show posted results in multi column table

function adibf_webform_submission_render_alter(&$renderable) {
 if (isset($renderable['#submission'])) {
  foreach (element_children($renderable) as $key) {
   $tableRows = array();
   
   if ($renderable[$key]['#webform_component']['type'] == 'table_element') {
    $cid = $renderable[$key]['#webform_component']['cid'];
    
    foreach($renderable['#submission']->data[$cid]['value']['rows'] as $row) {
     $data = strip_tags($row[0]);
     $quantity = $row['quantity'];
     
     $objData = json_decode($data);
     $subTotal = $objData->rate * $quantity;
     $tableRows[] = array($objData->ID, $objData->title, $objData->rate, $quantity, $subTotal);
    }
    
    $tableHeader = array('ID', 'Title', 'Rate', 'Quantity', 'Sub Total');
    
    $theTable = array(
     'header' => $tableHeader,
     'rows' => $tableRows,
    );
    
    $renderable[$key]['#markup'] = theme('table', $theTable);
   }
  }
 }
}

First code block will display form element visually in a multi column table with one text field for quantity in each row, the second block is for the posted results.

d) The last step is to add a little jQuery somewhere in the theme so after entering quantity, sub total column value would be changed. You can also add a feature to show Net Total somewhere on the page:

jQuery('.webform-component-table_element .form-text').change(function() {
  var elems = jQuery('.webform-component-table_element .form-text');
  
  var totalAmount = 0;
  for(var i=0; i<elems.length; i++) {
   totalAmount += table_element_calculate(elems[i]);
  }
  
  jQuery('#net_total').html(totalAmount);
 });


function table_element_calculate(elem) {
 var sPrice = jQuery(elem).parents('td').prev().text();
 var hTotal = jQuery(elem).parents('td').next();
 
 var p = 0;
 
 try {
  p = sPrice * jQuery(elem).val();
  
  if(isNaN(p))
   p = 0;
  
  if(p > 0)
   hTotal.find('.sub-total').html(p);
  // alert(p);
 } catch(e) {}
 
 return p;
}

Note: I just copied pasted these codes from a project that I did some months ago, so if something don't work please post in comments. Thanks.

Sunday, November 13, 2011

Creating separate sub menu in WordPress 3

I needed to create a split menu for one of my projects where top level menu items were in a horizontal menu and children of active menu in a sidebar. Its a super simple task in Joomla but I couldn't find anything out of the box in WordPress.



I managed to resolve this issue using wp_list_pages, but it was not sorted according to custom menu sort order. So I made this simple custom walker and it worked.

(This link helped me in adding has_children property to menu items).


class SplitMenu_Walker_Nav_Menu extends Walker_Nav_Menu {
 var $startMenu = false;
 
 function start_lvl(&$output, $depth) {
  
 }

 function end_lvl(&$output, $depth) {
  $this->startMenu = false;
 }
 
 function start_el(&$output, $item, $depth, $args) {
  if(($args->has_children && $item->current) || $item->current_item_parent)
   $this->startMenu = true;
  
  if($this->startMenu && $depth > 0)
    parent::start_el($output, $item, $depth, $args);
 }

 function end_el(&$output, $item, $depth) {
  if( $this->startMenu )
    parent::end_el($output, $item, $depth);
 }
 
 function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) {
        $id_field = $this->db_fields['id'];
        if ( is_object( $args[0] ) ) {
            $args[0]->has_children = ! empty( $children_elements[$element->$id_field] );
        }
        return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
    }
}

This code is tested with single level of sub menus only.

How to use:

  1. Copy above code and paste into your theme's functions.php
  2. Pass an object of walker class to your 'wp_nav_menu' function:

wp_nav_menu(
    array (
        'menu'            => 'main-menu',
        'walker'          => new SplitMenu_Walker_Nav_Menu
    )
);