[development] Forms API "Widgets", AJAX and non-JavaScript

Dave Cohen drupal at dave-cohen.com
Thu Feb 1 04:41:49 UTC 2007


On Tuesday 30 January 2007 20:45, Nedjo Rogers wrote:
> > I've done a couple of projects that needed custom "widgets" to use within
> > Forms API
>
> 2. Define a custom #type, so that you will have full control over output
> (e.g., define your own theme function). This is initially attractive, but
> in the end a limited approach, since you lose the existing properties of a
> #type.
>

I pounded my head against this issue.   I created a module to link various 
drupal objects together in flexible ways, using a concept of triples 
(subject, predicate, object) which has been mentioned on this list before.  I 
wanted a special widget to connect nodes together.   In my case, I wanted a 
node edit form to show list other nodes connected to the current node as 
checkboxes, and also provide a textarea where new node titles could be typed 
to add to the list.

So to define my form element, I wanted something like the following.  In this 
case, we're editing a node of type 'site', which has one or more 'master' 
nodes associated with it, as founders.  Notice the form element is of 
type 'triples_node_set' and the relevant data is passed in an attribute 
called '#triples'.

  $form['founder'] = 
    array('#type' => 'triples_node_set',
          '#title' => t('Founding Master'),
          '#description' => t('Which master founded this site?  <br/>Use the 
text field below to add a name.  Enable javascript for auto-complete feature.  
<br/>Note that you must first enter the master\'s name in the database.  
[%link]<br/>Note also that the preview button will not reflect changes made 
here.  You must hit submit to see the changes.',
                              array('%link' => l(t('add 
master'), 'node/add/jonang_master', array('target' => '_blank')))
          ),
          '#triples_object_node_type' => 'jonang_master',
          '#triples' => array('subject_type' => 'node',
                              'subject_id' => $node->nid,
                              'predicate' => 'founded_by',
                              'object_type' => 'node',
          ),
    );


So the trick is to preserve all that '#triples' info with the form when it is 
submitted.  The work is done in the process function.  In this case, we build 
a set of checkboxes and a textfield, wrapped in a fieldset.  But most 
importantly, what our process function returns includes the original $element 
(which has the #triples data) and it artificially sets the '#parents' array 
so that the data can be found later.  It's a lot trickier than it would 
ideally be, but it can be done.


function triples_node_set_process($element) {
  //drupal_set_message('triples_node_set_process' . dprint_r($element, 1));
  $data = $element['#triples'];
  $triples = triples_load_all($data);
  if ($triples[$data['predicate']]) {
    // display a checkbox for existing relationships
    foreach ($triples[$data['predicate']] as $triple) {
      $node = node_load($triple->object_id);
      $checkboxes[$triple->triple_id] = array('#type' => 'checkbox',
                                              '#title' => $node->title,
                                              '#default_value' => 1,
      );
    }
  }

  
  // display a form for creating new relationships
  // use comma-separated list of node titles
  // TODO: autocomplete
  $addfield = array('#type' => 'textfield',
                    '#title' => t('Add'),
                    '#description' => t('Comma-separated list of names'),
                    '#autocomplete_path' => 'triples/autocomplete/node/' . 
$element['#triples_object_node_type'],
  );

  // put everything in a fieldset
  $fieldset = $element;
  $fieldset['#type'] = 'fieldset';
  $fieldset['checks'] = $checkboxes;
  $fieldset['add'] = $addfield;
  $fieldset['#collapsible'] = TRUE;
  $fieldset['#tree'] = TRUE;
  unset($fieldset['#process']);

  return array('#tree' => TRUE,
               // include the converted element in the form
               'element' => $fieldset,
               // also have the form return the extra data we will need later
               'triples' => array('#type' => 'value',
                                  '#value' => $element,
                                  '#parents' => array('triples',
                                                      'triples_'._triples_count(),
                                  ),
                                  
               ),
               '#weight' => $fieldset['#weight'],
  );
  
}


Here's a submit function which finds and uses the #triples data passed in the 
original form element:
function triples_form_submit($form_id, $form_values) {
  //drupal_set_message("triples_form_submit($form_id)". dprint_r($form_values, 
1));
  // save all triples in the form:
  if (count($form_values['triples'])) {
    foreach ($form_values['triples'] as $element) {
      $value = $form_values;
      // learn the submitted value
      foreach ($element['#parents'] as $key)
        $value = $value[$key];
      // learn the triples data
      $data = $element['#triples'];
      // debug
      //drupal_set_message('triples_submit_form, value and triples data:' . 
dprint_r($value, 1) . dprint_r($data, 1));
      // invoke handler
      $func = $element['#type'] . '_submit';
      if (function_exists($func)) {
        $func($element, $value);
      }
    }
  }
}



More information about the development mailing list