Programmatically Creating Forms with Views in Drupal 7

December 2012

With Views 3, the Views’ team introduced a clever technique for creating forms, using the Form API, with Views. Views Bulk Operations uses this method as does the D7 version of editablefields (those modules might be a good place to start if you aren’t a developer and just landed here from the Googs).

In this example, I’ll discuss how to implement this technique in your own custom code. This example assumes you know how to create a module in Drupal and that you have programmatically created (or at leasted altered) forms using the Drupal Form API. You should have experience creating Views through the UI. Some experience playing with Views in code might be helpful but is probably not necessary.

Why would you want to do this?

  1. you want to give users some ability to create custom forms using the Views UI;
  2. you need a form that’s a list of data;
  3. you want to use AJAX to smoothly reload a View based on user input (or elapsed time or some browser-side event) (I’ll demonstrate this in a second part).

In this example you'll:

  1. create a simple View, a list of node information;
  2. create a custom Views’ field handler to render a textfield for editing the node titles;
  3. create validation and submission handlers for our form.

This is what you’ll end up with:

First, create the View. I decided to create a table view of my nodes with some basic information:

Not much to say here. Just your typical, run-of-the-mill View. You’ll replace the title field with our custom form field.

Next, create a starting module with a .info and a .module file. I called mine module “view_form” (in real life, I’d probably choose a name less likely to collide with another module).

This is the only code inside the .module file:

<?php
/**
 * Implements hook_views_api().
 */
function view_form_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'view_form') . '/views',
  );
}
?>

This code implements “hook_views_api” and tells Views where it can find your “[module].views.inc” file, which will contain my Views’ hook implementations. As is typical, the “view_form.views.inc” file is located in the “views” directory under the root of the module.

The view_form.views.inc contains just one Views’ hook implementation:

<?php
/**
 * Implements hook_views_data_alter().
 *
 * @param $data
 *   Information about Views' tables and fields.
 */
function view_form_views_data_alter(&$data) {
  // Add the Title form field to 
  $data['node']['title_edit'] = array(
    'field' => array(
      'title' => t('Title form field'),
      'help' => t('Edit the node title'),
      'handler' => 'view_form_field_handler_title_edit',
    ),
  );
}
?>

Use “hook_views_data_alter” to add a new “field” to the “node” element provided by Views. The node element primarily consists of properties and fields defined by the Schema and Field APIs. But other modules can add “dummy” fields—fields that don’t provide any data of their own but process and render values already defined by the Node and Field modules.

The practical effect of “view_form_views_data_alter” is that—after clearing caches—there will be one new field to add to our node Views in the Views UI:

You could go ahead and add that field—but Views will complain because it can’t find the code for the “view_form_field_handler_title_edit” field handler.

So go ahead and create a “view_form_field_handler_title_edit.inc” file inside of the “views” sub-directory. The module directory structure is now complete and looks like this:

Next, edit the .info file and add the following the line.

files[] = views/view_form_field_handler_title_edit.inc

This line tells Drupal where to find and auto-load our field handler whenever Views needs it.

Now add this code inside “view_form_field_handler_title_edit.inc”:

<?php
/**
   * @file
   *
   * A Views' field handler for editing a node title.
   *
   */
class view_form_field_handler_title_edit  extends views_handler_field {
  function construct() {
    parent::construct();
    $this->additional_fields['nid'] = 'nid';
    $this->additional_fields['title'] = 'title';
  }

  function query() {
    $this->ensure_my_table();
    $this->add_additional_fields();
  }
}
?>

This is the basic class definition for your field handler. It extends “views_handler_field” and adds constructor and query methods to make sure the node id and title values are loaded into the view.

Now, add the following “render” method to the class…this is when a little magic happens.

<?php
/**
   * @file
   *
   * A Views' field handler for editing a node title.
   *
   */
class view_form_field_handler_title_edit  extends views_handler_field {
  ...
  function render($values) {
    // Render a Views form item placeholder.
    // This causes Views to wrap the View in a form.
    // Render a Views form item placeholder.
    return '<!--form-item-' . $this->options['id'] . '--' . $this->view->row_index . '-->';
  }
  ...
}
?>

The render method returns the html (or other markup) to display for the field. To output a Views’ managed form field, you render an HTML comment in the form in this format:
<!—form-item-FIELD_ID—ROW_INDEX—> 
This syntax mirrors the form array structure you’ll use to provide replacement form fields views. When Views’ sees that your field handler renders a comment like above, it will initialize and wrap a form around the view then call your handlers “views_form” method:

<?php
class view_form_field_handler_title_edit extends views_handler_field {
  ...
  /**
   * Add to and alter the form created by Views.
   */
  function views_form(&$form, &$form_state) {
    // Create a container for our replacements
    $form[$this->options['id']] = array(
      '#type' => 'container',
      '#tree' => TRUE,
    );
    // Iterate over the result and add our replacement fields to the form.
    foreach($this->view->result as $row_index => $row) {
      // Add a text field to the form.  This array convention
      // corresponds to the placeholder HTML comment syntax.
      $form[$this->options['id']][$row_index] = array(
        '#type' => 'textfield',
        '#default_value' => $row->{$this->aliases['title']},
        '#element_validate' => array('view_form_field_handler_title_edit_validate'),
        '#required' => TRUE,
      );
    }
  }
  ...
}
?>

The “views_form” method allows you to alter the form created by Views. Add your field replacements by creating a container element with an index that matches the field id ($this->options[‘id’]). Then iterate over the view result and add a form field element to the container for every row in the view, using the row index as the element key in the container. Views matches this form array structure to the corresponding HTML comment to insert the field in the rendered View. In other words:
<!—form-item-FIELD_ID—ROW_INDEX—> 
is replaced by:

<?php
$form[FIELD_ID][ROW_INDEX];
?>

The above “views_form” method adds a textfield to every row pre-filled with the node title. I’ve specified a “element_validate” handler to the field because Views doesn’t wire up a form-wide validate handler.

Create your element validation callback OUTSIDE the class but in the same .inc file so that it is available in the global context but is only loaded with the field handler:

<?php
class view_form_field_handler_title_edit extends views_handler_field {
  ...
}
/**
 * Validation callback for the title element.
 *
 * @param $element
 * @param $form_state
 */
function view_form_field_handler_title_edit_validate($element, &$form_state) {
  // Only allow titles where the first character is capitalized.
  if (!ctype_upper(substr($element['#value'], 0, 1))) {
    form_error($element, t('All titles must be capitalized.'));
  }
}
?>

This simple validator just makes sure that the node titles are always capitalized.

Finally, add a “views_form_submit” method to the field handler:

<?php
class view_form_field_handler_title_edit extends views_handler_field {
  ...
  /**
   * Form submit method.
   */
  function views_form_submit($form, &$form_state) {
    // Determine which nodes we need to update.
    $updates = array();
    // Iterate over the view result.
    foreach($this->view->result as $row_index => $row) {
      // Grab the correspondingly submitted form value.
      $value = $form_state['values'][$this->options['id']][$row_index];
      // If the submitted value is different from the original value add it to the
      // array of nodes to update.
      if ($row->{$this->aliases['title']} != $value) {
        $updates[$row->{$this->aliases['nid']}] = $value;
      }
    }

    // Grab the nodes we need to update and update them.
    $nodes = node_load_multiple(array_keys($updates));
    foreach($nodes as $nid => $node) {
      $node->title = $updates[$nid];
      node_save($node);
    }

    drupal_set_message(t('Update @num node titles.', array('@num' => sizeof($updates))));
  }
  ...
}
?>

The “views_form_submit” method works much like any Form API submit callback. In the case above, you iterate over the view result to identify submitted changes, then iterate over the changes to update the corresponding nodes.

A note about the use of “$this->aliases”: this is one way that Views ensures that different field handlers don’t collide. Think about it, the Views UI allows you to add any number of fields called “title”, any other form field handler, or even another instance of this handler. To allow this, Views indexes the result objects with unique field aliases, so you should use the syntax “$row->{$this->aliases[‘title’]}” when grabbing results to make sure you get the right “title”.

So that’s it. You’ve now created a View form. There are plenty of places to you could take this. You could easily substitute multiple form elements by adding additional fields to your container. You could use the Form API “#ajax” element to handle form submissions without a page refresh. You could include a submit button on every row and only allow one row to be updated at a time.

The example code is available in my Drupal.org sandbox. http://drupalcode.org/sandbox/krlucas/1872176.git/tree

 

This post was written by former Isoveran Kelly Lucas.