background image

Content tagged with: usability

Eric's picture

For some time now I've wanted to write a blog entry about using AHAH to create dynamically generated form elements. After a recent conversation at work regarding usability, I now had a real world example to create: how to use tiered taxonomy to dynamically generate a form. This code snippet will show you how to create a form that creates child select dropdowns based on the parent taxonomy term the user selects.

First I established a multi-tier taxonomy called "AHAH":

For this example I created a menu callback to display my initial form:

<?php
function helper_menu() {
 
 
$items = array();
 
 
$items['ahah-form'] = array(
   
'title' => 'AHAH Form',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('_helper_callback_ahah_form'),
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
  return
$items;
 
}
?>

I then defined the page callback to show the initial form:

<?php
function _helper_callback_ahah_form() {

 
// define an array to contain form elements
 
$form = array();
 
 
// define the top level vid
 
$vid = 2;
 
 
// fetch a tree of taxonomy elements
 
$tree = taxonomy_get_tree($vid, 0, -1, 1);
 
 
// loop though taxonomy and collect elements
 
$options = array();
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// create the first select dropdown input
 
$form['select_1'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 1'),
   
'#size' => 5,
   
'#multiple' => false,
   
'#ahah' => array(
     
'event' => 'change',
     
'path' => 'ahah-form-callback',
     
'wrapper' => 'wrapper-1',
     
'method' => 'replace',
    ),
  );
 
 
// pass the top level vid in the form
 
$form['ahah_vid'] = array(
   
'#type' => 'hidden',
   
'#value' => $vid,
  );
 
 
// create an empty form element to contain the second taxonomy dropdown
 
$form['wrapper_1'] = array(
   
'#prefix' => '<div id="wrapper-1">',
   
'#suffix' => '</div>',
   
'#value' => '&nbsp;',
  );
 
 
// add a form submit button
 
$form['submit'] = array(
   
'#value' => 'Submit',
   
'#type' => 'submit'
 
);
 
  return
$form;
 
}
?>

The above form callback produces the following:

Next, I defined a callback to handle the AHAH page request:

<?php
// new menu item:
function helper_menu() {
 
 
// ...
 
 
$items['ahah-form-callback'] = array(
   
'title' => 'AHAH Form Callback',
   
'page callback' => '_helper_callback_ahah_form_callback',
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
 
// ...

 
return $items;
 
}

// and, here's the AHAH callback used to create the new form elements:
function _helper_callback_ahah_form_callback() {
 
 
// define a string variable to contain callback output
 
$output = "";
 
 
// pull the top level vid from the $_POST data
 
$vid = $_POST['ahah_vid'];
 
 
// pull the selected dropdown from the $_PODT data
 
$parentVid = $_POST['select_1'];
 
 
// loop through the taxonomy tree and fetch child taxonomies
 
$options = array();
 
$tree = taxonomy_get_tree($vid, $parentVid, -1, 1);  
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// define the second tier select dropdown element
 
$form['select_2'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 2'),
   
'#size' => 5,
   
'#multiple' => false,
  );
 
 
// rebuild form object and output new form elements
 
$output .= ahah_render($form, 'select_2');
 
 
// render form output as JSON
 
print drupal_to_js(array('data' => $output, 'status' => true));
 
 
// exit to avoid rendering the theme layer
 
exit();
 
}

// Lastly, here's a help function pulled from Nick Lewis's blog to alter the form
// see: http://www.nicklewis.org/node/967
// NOTE: based on poll module, see: poll_choice_js() function in poll.module
function ahah_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);

 
// Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form);
}
?>

The above code allows the user to select an option from the top level tier of taxonomy and the AHAH callback will generate the a select dropdown of the child taxonomies as shown below:

On form submission, you'll see that the options the user selected as stored in $form_state['values']['select_1'] and $form_state['values']['select_2']

Eric's picture

This article will show you how to establish parent and child relationships between nodes. To start, you'll need to install and enable the CCK module, and node reference (which comes with CCK). Next, you'll need to create two node types (admin/content/types/add). For my example, I created two node types: Organization (parent) and Department (child), both consisting of a title and body (of course, you can add as many fields as you'd like).

Next, you'll want to add a new field to the parent node (organization) by editing the content type (admin/content/node-type/organization) and clicking on the "Manage fields" tab. Enter the information to add a new reference field on this screen. Add a label (ex: "Department Reference"), enter a field name (ex: field_ref_department), choose "Node reference" as the field type, and choose "Autocomplete text field" as the form element as shown below.

node reference add field 1

On the next screen you can choose the field options. For this example, I changed "Number of values" to "Unlimited" (to allow a one to many relationship), selected "Department" in the list of content types that can be referenced, and saved these settings.

node reference add field 2

Now, if you create a new parent node (Organization: node/add/organization), you'll have the option of adding pre-existing child (Department) nodes. As you start typing the title of the child nodes in the node reference auto-complete field, it will automatically populate the field with nodes that match. Of course this will only work if you have already created child nodes. At this point adding relationships is a two step process.

node reference add parent node 1

To improve usability, you can install the Popups API (http://drupal.org/project/popups) and Add and Reference (http://drupal.org/project/popups_reference) modules which allow you to add child nodes without leaving the node/add/PARENT-NODE screen. After installing and configuring these modules, you'll be able to add child nodes directly from the node/add/PARENT-NODE screen by clicking on the "Add new: Add Child Node link", which spawns a lightbox window to add a child node.

node reference add parent node 2

Here is a screenshot of a parent node view after adding child relationships:

node reference parent node view

Eric's picture

If someone clicks on a taxonomy term they land on a page showing all the content tagged with that term. The title on those pages is simply the taxonomy term. I thought it would be more usable if I changed the verbiage of the title to let the user know what they are viewing. This code snippet shows how you can change the title for the taxonomy term landing pages:

<?php
function MYTHEME_preprocess_page(&$variables) {
  if (
arg(0)=='taxonomy' && arg(1)=='term') {
   
$variables['title'] = "Content tagged with: " . $variables['title'];
  }
}
?>

Eric's picture

When you have a view that has a lot of results, you could improve usability by showing how many total results there are and how many are being shown on the current page. Similar to this:

Displaying ### - ### of ### results

I created a function that accepts two arguments: the view name and the view's display_id (default, block, page, etc). The function loads the view, executes it, and calculates these numbers for you.

<?php
function _MYMODULE_view_totals($viewName, $display_id = 'default') {

 
// load viewobject
 
$view = views_get_view($viewName);

 
// ensure view exists
 
if (!$view) return;

 
// set object property to return total rows 
 
$view->get_total_rows = true;

 
// set display_id
 
$view->set_display($display_id);

 
// execute view
 
$view->execute();

 
// acquire data from views object and $_REQUEST   
 
$itemsPerPage = $view->pager['items_per_page'];
 
$currentPage = $_REQUEST['page']+1;
 
$total = $view->total_rows;

 
// start calculation   
 
$start = 10*$currentPage-9;
 
$end = $itemsPerPage * $currentPage;
  if (
$end>$total) $end = $total;
  
 
// return html
 
return "Displaying $start - $end of $total";

}
?>

There are a few places you can insert this code. I decided to put it directing in my view, right after my exposed filters. To do that, I created a new file in my theme called: "views-view--MYVIEWNAME--page.tpl.php", and used the "Theme Information" section on the edit view screen to populate it with the default html & PHP. I searched for the code that inserts the exposed filters ($exposed), and inserted the following:

<?php
if (function_exists('_MYMODULE_view_totals')) {
 
$viewTotals = _MYMODULE_view_totals('MYVIEWNAME', $display_id = 'page_1');
  if (
$viewTotals) {
    print
"<div class='MYTHEME_views_totals'>$viewTotals</div>";
  }
}
?>

I enclosed it in a div with a class name so I could adjust the CSS as necessary.

Eric's picture

I've been working on an intranet site that needed to have typical intranet permissions: unauthenticated users can see a handful of pages and the rest of the nodes are only visible to authenticated users. Instead of having the user specify permissions for every page, I figured it would be more usable to have them specify a list of pages available to unauth users.

I created an admin settings page callback to generate the form with a single textarea input. Users will enter a list of URLs in the textarea, one per line:

<?php
function MYMODULE_menu() {
 
$items = array();

 
$items['admin/settings/MYMODULE'] = array(
   
'title' => 'MYMODULE Settings',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('_MYMODULE_callback_admin_settings'),
   
'type' => MENU_NORMAL_ITEM,
   
'access arguments' => array('administer site configuration'),
  );

  return
$items;
}

function
_MYMODULE_callback_admin_settings() {
 
$form = array();

 
$form['MYMODULE_unauth_pages'] = array(
   
'#type' => 'textarea',
   
'#title' => 'Unauth Pages',
   
'#default_value' => variable_get('MYMODULE_unauth_pages',''),
  );
   
  return
system_settings_form($form);
}
?>

For this example, I entered the following URLs in the textarea:

<front>
about-us
contact-us

I then added a menu_alter hook function to override the access control for viewing nodes:

<?php
function MYMODULE_menu_alter(&$items) {
 
// per unauth pages, replace the callback function
 
if (function_exists('_MYMODULE_node_access')) {
   
// note: access callback function was previously: node_access
   
$items['node/%node']['access callback'] = '_MYMODULE_node_access';
  }
}
?>

And, then added a new access control function:

<?php
function _MYMODULE_node_access($op, $node) {

 
// check if user is unauth
 
if (in_array('anonymous user', array_values($GLOBALS['user']->roles))) {

   
// get a list of unauth pages
   
$unauth = variable_get('MYMODULE_unauth_pages','');
   
$unauth = explode("\r\n", trim($unauth));
       
   
// replace <front> with empty string
   
if (in_array('<front>', $unauth)) {
     
$unauth[array_search('<front>',$unauth)] = '';
    }
       
   
// check for unauth entries
   
if (is_array($unauth) && count($unauth)) {
     
// check if current url is allowed
     
if (!in_array($_REQUEST['q'], $unauth)) {
        return
false;
      }           
    }
  }
   
 
// default to node_access function result
 
return node_access($op, $node);
   
}
?>

Now, unauth users have access ONLY to the pages you define and the rest of the node viewing permissions default to the node_access function.