Programmatically Creating Forms with Views in Drupal 7

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

Comments

I had no idea the views forms stuff was so accessible. Thanks for putting this together!

Why is this done with some strange HTML comment format rather than some parameter? Arggh! The number of strange naming conventions one needs to grasp in drupal just keeps getting bigger..

Yeah, it's a little weird. Since Views will be in core in D8 maybe a tighter integration between the Form and Views APIs is in the offing.

But to answer your question more directly, HTML comments are used so that the Views query and pre-processed output can still be cached. You could implement your own string replacement format using hook_views_form_substitutions (http://api.drupal.org/api/views/views.api.php/function/hook_views_form_s...).

Will this option work for views 3 on drupal 6? If so, then this article is the best piece of info I have ever come across for drupal dev.

That's a good question. I think there's a 75% chance it would work :-) but haven't had a chance to test. You can try the module linked to at the end of the post; hopefully you'll just need to edit the .info file and change to D6.

Even though I'm not facing a form challenge, I liked this tutorial in that it shows some framework routines for writing Views plugins. Would you care && find time to write a similar tutorial on how to write a custom filter for Views?

Thanks Artur! That's a really great idea.

One thing to remember about Views is that the fastest way to get started with making a custom plug-in is to just copy and paste the code of another plug-in in your own module and do some search and replace.

It's a practice that we all do all the time with the Views' theme templates. That's why I'm excited for the core plug-in architecture in D8.

This tutorial is great, but it doesn't cover the final steps. That is, once you have these form elements in a view, how do you connect them to an actual form? I still have some questions.

Where did that "Save" button come from in your screen shot? Does a "Save" button magically appear on each view, or did you add it separately?

I want multiple views on the same page (via multiple blocks, panels, or whatever). Can I have one submit button for each view? What if I want just one submit button for the whole page?

How and when does views_form_submit() get called?

Most of these questions would probably be answered with an explanation how how to tie all the form fields into an actual form, which is probably pretty simple.

The answer is simple. You don't have to do anything. Views handles wrapping the View in a form. Views adds the save button automatically.

You can have multiple Views on a page and each will be wrapped in a single form.

So do you think it's possible to add this views form to for example an entity form?

Hi, why the images are not visibles ?

M.

Sorry about that...Skitch/Evernote changed something. Images are fixed!

Hi Kelly,

First off, thanks for this. Once I figure it out, it's going to prove a big help.

I've cloned your code, installed the module, added the field and it looks great. I see it in my view, I have the submit button, but when I change the data, nothing is edited.

I've tried adding print "test'; exit; to the view_form_field_handler_title_edit_validate function and nothing happens (I'm assuming it would, still new to the view api), so it seems the element_validate isn't being called. I am drupal 7, views 3, so we're on the same page there and I've done no edits to your code.

Any theory? Should my exit; show TEST if all fired properly?

Thanks!

Hi Kelly,

I wrote a couple hours ago about not validating. I just wanted to report that in does, in fact validate when I create a new view page.

For some reasons it's still not working where I'm trying to get a block to appear on a content page. My view block (with your field) will be on this page http://mysite.local:8888/node/26, but when I submit, I'm redirected to another page and the changes don't occur.

Anyway, I wanted to follow up and let you know my previous message was a non-issue (as I'm sure this one will turn out being soon enough).

Thanks again for such a helpful tutorial.

Hmmm. Not sure why it's not working in a block, but I'll check it out.

I've updated the git sandbox with a fix for this:
http://drupalcode.org/sandbox/krlucas/1872176.git/commitdiff/5469ab7afa4...

Basically I just added the following lines to the end of the views_form method in the field handler:

// Submit to the current page if not a page display.

if ($this->view->display_handler->plugin_name != 'page') {

$form['#action'] = current_path();

}

how to do this? "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".
I wanted to use ajax submit either 'nid" or "title" and get all other fields in "read only"state

also add blank some thing like this http://phpmysqlmania.blogspot.in/p/dynamic-table-row-inserter.html but only blank row
use case is for Purchase order where user searches a product by tile or by id(product number and all other fiedls are populated and user needs to enter quantity value and the total value is populated(rate per unit is predetermined perhaps i'll use computed field.
Could you please explain how to achieve this?

Thank you for this step by step tutorial!
I works perfect, but only on a page. When I tried in to do this with a block, it doesn't work. (I saw that the form action goes to /)

Thanks Adrian. I'll see if I can make a tweak to make it work in a block.

Hi Adrian, See my reply above for a fix.

first of all thanks for this perfect step by step tutorial. it really saved me from all the headache probably I should endure to configure how to convert a views list to a form.
I have one question, I have added custom fields to my node type (e.g field_address), how should I replace the 'title' with this name? should I use field_ before it to laod the data in the textfield?

Updating custom fields are a little trickier. It depends on the field type as well. But for a custom text field without translations:

$node->field_name_of_custom_field[LANGUAGE_NONE][0]['value'] = $form_state['values']['field_name'];
node_save($node);

or you could install Entity API which would allow you to do this and don't have to care quite as much about the field type:

$node_wrapper = entity_metadata_wrapper('node', $node);
$node_wrapper->field_name_of_custom_field = $form_state['values']['field_name'];
node_save($node);

tanx a lot for replying so fast. it was really helpful.

I'm sorry to bother you again. can you also help me about the additional_fields? how should I change this part for the custom fields?
$this->additional_fields['title'] = 'title';

Sorry for the late reply. Try something like this:

function construct() {
    parent::construct();
    $this->additional_fields = array(
      'my_custom_field' => array(
        'table' => 'field_data_field_my_custom_field',
        'field' => 'field_my_custom_field,
      ),
    );
  }

Thanks for a great tutorial! I got this working with Drupal 6 and Views 3. There were actually minimal differences.

I've noticed two issues so far that I was hoping you might be able to help with:

1. After I submit the form the view doesn't refresh. So it's currently not possible to display the field differently upon submission.

2. When I go to the next page, it thinks I'm acting on items in the first page.

Any ideas?

I realized that both issues were related to the same problem: I wasn't submitting the values correctly. I've since moved this into production and everything is working well.

I did have one remaining question, though, in my original build, I wasn't able to get multiple FAPI fields to display for one view field handler. I tried a number of different configurations.

Is this something you've been able to implement?

Really thanks for this post, worked perfect with me and could easily change custom fields as well.
can you please give small example for these two things:
"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."

- using ajax
- individual submit buttons

Thanks again

Hi

this tutorial is very useful. I try to adapate it to edit 2 custom fields in my view

i modify the views.inc file

function pronostic_views_data_alter(&$data) {
// Add the score1 and score 2 form field to the node-related fields.
$data['node']['score_1'] = array(
'field' => array(
'title' => t('Score_1 form field'),
'help' => t('Edit the node score_1'),
'handler' => 'pronostic_field_handler_scores_edit',
),
);

$data['node']['score_2'] = array(
'field' => array(
'title' => t('Score_2 form field'),
'help' => t('Edit the node score_2'),
'handler' => 'pronostic_field_handler_scores_edit',
),
);

}

an the mymodule_field_handler_myfields_edit.inc file

<?php

/**
* @file
*
* A Views' field handler for editing a node title.
*
*/
class pronostic_field_handler_scores_edit extends views_handler_field {

function construct() {
parent::construct();
$this->additional_fields['nid'] = 'nid';
$this->additional_fields = array(
'score_1_field' => array(
'table' => 'field_data_field_score_1',
'field' => 'field_score_1'
),
'score_2_field' => array(
'table' => 'field_data_field_score_2',
'field' => 'field_score_2'
),
);
}

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

/**
* Render the field contents.
*
* @param $values
* @return string
*/
function render($values) {
// Render a Views form item placeholder.
return '';
}

/**
* Add to and alter the form.
*/
function pronostic(&$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(
array(
'#type' => 'textfield',
'#default_value' => $row->{$this->aliases['score_1']},
'#element_validate' => array('pronostic_field_handler_scores_edit_validate'),
'#required' => TRUE,
);
array(
'#type' => 'textfield',
'#default_value' => $row->{$this->aliases['score_2']},
'#element_validate' => array('pronostic_field_handler_scores_edit_validate'),
'#required' => TRUE,
);
);
}

// Submit to the current page if not a page display.
if ($this->view->display_handler->plugin_name != 'page') {
$form['#action'] = current_path();
}
}

/**
* Form submit method.
*/
function pronostic_submit($form, &$form_state) {
// Determine which nodes we need to update.
$updates = array();
foreach($this->view->result as $row_index => $row) {
$value = $form_state['values'][$this->options['id']][$row_index];
if ($row->{$this->aliases['score_1']} != $value1 AND $row->{$this->aliases['score_2']} != $value2) {
$updates[$row->{$this->aliases['nid']}] = array($value, $value2);
}
}

// Grab the nodes we need to update and update them.
$nodes = node_load_multiple(array_keys($updates));
foreach($nodes as $nid => $node) {
$node->field_score_1['und'][0]['value'] = $updates[$nid][0];
$node->field_score_2['und'][0]['value'] = $updates[$nid][1];
node_save($node);
}

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

}

/**
* Validation callback for the title element.
*
* @param $element
* @param $form_state
*/
function pronostic_field_handler_scores_edit_validate($element, &$form_state) {
// Only allow numeric.
if (!is_numeric($element['#value'])) {
form_error($element, t('All score must be numeric.'));
}
}

But it doesn't work

If anyone could hel

thank you so much

Hello, it it possible to remember the values I change if I go to another page or refresh ? I'm sure Drupal can do this, but I don't have a clue were to start.

Pages

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Type the characters you see in this picture. (verify using audio)
Type the characters you see in the picture above; if you can't read them, submit the form and a new image will be generated. Not case sensitive.