VBO for D7, with a little help from my Drupal friends

I've been a lazy adopter of Drupal 7. I admit it, mea culpa, but I'll leave this discussion to another post. What happened in the mean time is that VBO for D7 languished for a year or so in a zombie state until it was rescued in early May by Bojan Živanović, bojanz on d.o. He started out by announcing a sandbox project containing his code, and a few days later, I was convinced that this was the way to go for VBO on D7. He agreed to become co-maintainer for VBO in charge of D7 - maybe unaware of what awaited him in terms of issue queue support!

Bojan made not one, but two radical changes to VBO, solving a long-standing issue and enhancing Views itself with a cool new feature. Let me explain:

  • VBO was initially designed by yours truly as a style plugin. It obviously worked, but the side effect was that a VBO could only be displayed as a table (implemented by the module itself), not as a grid, carousel, or any other crazy Views style. In hindsight, this was not the ideal choice since what was needed was only to add a checkbox to each row, which is really a field thing. That's what Bojan did: package the VBO functionality in a Views field handler instead of a style plugin.

  • To achieve this, Views core needed to be retrofitted with the ability to display forms during its own rendering. Again, Bojan built a proof of concept that was later submitted as a patch to Views and committed to Views 3 on both D6 and D7. Let's look at what this means for a Views developer:

There's an example Views 3 plugin for D6 that illustrates the idea of embedding form elements in a view. This example module provides a new field that exposes an HTML text input. The view automatically creates a multistep form whose first page contains all the text inputs (one for each row) and adds a "Save" button. The module also adds validation and submission functions. Here's the code:

<?php
// @file views_form_example.views.inc

/**
* Implementation of hook_views_data_alter().
*/
function views_form_example_views_data_alter(&$data) {
 
$data['node']['views_form_example'] = array(
   
'title' => t('Views Form Example field'),
   
'help' => t('Demonstrates a form'),
   
'field' => array(
     
'handler' => 'views_form_example_handler_field',
    ),
  );
}

/**
* Implementation of hook_views_handlers().
*/
function views_form_example_views_handlers() {
  return array(
   
'info' => array(
     
'path' => drupal_get_path('module', 'views_form_example'),
    ),
   
'handlers' => array(
     
'views_form_example_handler_field' => array(
       
'parent' => 'views_handler_field',
      ),
    ),
  );
}
?>

Nothing new here, just a declaration of our field handler.

<?php
// @file views_form_example_handler_field.inc

class views_form_example_handler_field extends views_handler_field {
  function
query() {}

  function
render($values) {
    return
'<!--form-item-' . $this->options['id'] . '--' . $this->view->row_index . '-->';
  }

  function
views_form(&$form, &$form_state) {
   
// The view is empty, abort.
   
if (empty($this->view->result)) {
      return;
    }

   
$field_name = $this->options['id'];
   
$form[$field_name] = array(
     
'#tree' => TRUE,
    );
   
// At this point, the query has already been run, so we can access the results
   
foreach ($this->view->result as $row_id => $row) {
     
$form[$field_name][$row_id] = array(
       
'#type' => 'textfield',
       
'#title' => t('Your name'),
       
'#default_value' => '',
      );
    }
  }

  function
views_form_validate($form, &$form_state) {
   
$field_name = $this->options['id'];
    foreach (
$form_state['values'][$field_name] as $row_id => $value) {
      if (
$value == 'Bojan') {
       
form_set_error($field_name . '][' . $row_id, "You can't be named Bojan. That's my name.");
      }
    }
  }
}
?>

Things get a bit interesting here: The render() method return special syntax that instructs Views to expect a form element with the given identifier. Views then calls the views_form() method to allow the handler to create the form elements, making sure that the resulting identifiers match the ones returned by render. In the case of this example, the expected identified is obtained by concatenating the field identifier with the row index - because the form element's structure will generate these identifies via Form API. The handler also declares a form validation function.

Now for the module file itself:

<?php
// @file views_form_example.module

/**
* Implementation of hook_views_api().
*/
function views_form_example_views_api() {
  return array(
   
'api' => 3,
  );
}

/**
* Gets our field if it exists on the passed-in view.
*
* @return
*  The field object if found. Otherwise, FALSE.
*/
function views_form_example_get_field($view) {
  foreach (
$view->field as $field_name => $field) {
    if (
is_a($field, 'views_form_example_handler_field')) {
     
// Add in the view object for convenience.
     
$field->view = $view;
      return
$field;
    }
  }
  return
FALSE;
}

/**
* Confirmation step of the views multistep form.
*/
function views_form_example_confirmation_form($form, $form_state, $view, $output) {
 
$form = confirm_form($form,
   
t('Are you sure you want to give your name to total strangers?'),
    array(
'path' => $_GET['q'], 'query' => $view->get_exposed_input())
  );

  return
$form;
}

/**
* Implementation of hook_views_form_submit().
*/
function views_form_example_views_form_submit($form, &$form_state) {
 
$field = views_form_example_get_field($form['#parameters'][2]);
  if (!
$field) {
    return;
  }

 
// Advance to the confirmation form.
 
if ($form_state['storage']['step'] == 'views_form_views_form') {
   
$form_state['storage']['step'] = 'views_form_example_confirmation_form';
  }
  elseif (
$form_state['storage']['step'] == 'views_form_example_confirmation_form') {
   
drupal_set_message('We have a winner!');
   
drupal_goto($_GET['q']);
  }
}
?>

The interesting function here is views_form_example_views_form_submit() which gets called, not surprisingly, upon form submission. It finds the example field (the view object is passed to the $form structure) and then decides on the next step, like any multistep form handler. The difference is that the step name is actually the form function for the given step, which in our case is the confirmation form views_form_example_confirmation_form.

These changes made to Views are a generalization of what VBO did in D6. The fact that they were included in Views means that other modules will now be able to create specialized views forms and act upon them. In addition to VBO for D7, Drupal Commerce is also using those innovations.

Kudos to Bojan for his great work on VBO and Views. With his contribution, I am sure that VBO will rock even more on D7!

Comments

Thank you infojunkie, thank you bojanz. I just want to ask if the code in this page is going to work on a Drupal7 installation

Jeeesh finding this on the net was a REAL pint the docs on this stuff are almost non existent I knew views had forms added to it recently but finding how this is implemented is a problem try googling for this.

+1 getting that example module put into the project that is trying to replicate the examples module for views... http://drupal.org/project/views_plugin_examples

thanks for this post exactly what i needed.

For me, VBO is one of those required modules that goes in just about every Drupal installation I've done (at least since it's been available!)

Thanks Bojanz for doing the hard work to make this available in D7. Can't wait to see it, and I'm sure using it within other view styles will open up a ton of possbilities. Thanks!

After the Views issue queue, the VBO one is not even remotely scary :)

Just a few notes: 1) Turns out is_a() is deprecated in PHP 5.2 (but not PHP 5.3), so some users got strict errors, and VBO and Drupal Commerce switched to "instanceof". 2) Instead of using hook_views_form_submit(), it's better to register a custom form submit callback from a form alter (like VBO does). This was also a later "policy" change, because there's no need for that hook implementation to fire on every views form (and there might be plenty, apart from VBO, Drupal Commerce, and editablefields, more and more people are asking for API help and doing something with it).

There's some effort in progress in the Views queue to update the documentation to include what I mentioned above, as well as fix a bug with the exposed filters using ajax + views form (though I plan to add a workaround for that in VBO, it just needs an explicit $form['#action'] pointing to the current page or wherever).

The plan of July: less than 100 issues in the VBO queue, and a 7.x beta. I'm already very happy with VBO, a large part of that because of the awesome Rules integration that fago helped us write.

Thank you Karim for your co-operation, and of course VBO itself. It's easy to improve on something already awesome.