diff --git a/docs/docs.php b/docs/docs.php
index d29cd5e..f5bc8ba 100644
--- a/docs/docs.php
+++ b/docs/docs.php
@@ -286,7 +286,7 @@ function hook_views_handlers() {
function hook_views_api() {
return array(
'api' => 2,
- 'path' => drupal_get_path('module', 'example') . '/includes/views',
+ 'path' => drupal_get_path('module', 'example') . '/includes/views',
);
}
@@ -298,13 +298,13 @@ function hook_views_api() {
* auto-loaded. This must either be in the same directory as the .module file
* or in a subdirectory named 'includes'.
*
- * This hook requires an array of views, where each array has key/value pair and must
+ * This hook requires an array of views, where each array has key/value pair and must
* have $value == $view->name, it is invalid if the keys not match.
- *
+ *
* The $view->disabled boolean flag indicates whether the View should be
* enabled or disabled by default.
- *
- *
+ *
+ *
* @return
* An associative array containing the structures of views, as generated from
* the Export tab, keyed by the view name. A best practice is to go through
@@ -578,6 +578,32 @@ function hook_views_query_substitutions() {
}
/**
+ * This hook is called to get a list of placeholders and their substitutions,
+ * used when preprocessing a View with form elements.
+ */
+function hook_views_form_substitutions() {
+ return array(
+ '' => 'Example Substitution',
+ );
+}
+
+/**
+ * Views form (View with form elements) validate handler.
+ * Called for all steps ($form_state['storage']['step']) of the multistep form.
+ */
+function hook_views_form_validate($form, &$form_state) {
+ // example code here
+}
+
+/**
+ * Views form (View with form elements) submit handler.
+ * Called for all steps ($form_state['storage']['step']) of the multistep form.
+ */
+function hook_views_form_submit($form, &$form_state) {
+ // example code here
+}
+
+/**
* This hook is called at the very beginning of views processing,
* before anything is done.
*
diff --git a/help/api-forms.html b/help/api-forms.html
new file mode 100644
index 0000000..39d50b8
--- /dev/null
+++ b/help/api-forms.html
@@ -0,0 +1,77 @@
+Views allows handlers to output form elements, wrapping them automatically in a form, and handling validation / submission.
+The form is multistep by default, allowing other modules to add additional steps, such as confirmation screens.
+
+
Implementation
+A views handler outputs a special placeholder in render(), while the real form with matching structure gets added in views_form().
+When the View is being preprocessed for the theme file, all placeholders get replaced with the rendered form elements.
+
+The views handler can also implement views_form_validate() and views_form_submit().
+
+ 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' => '',
+ );
+ }
+ }
+
+ // Optional validate function.
+ function views_form_validate($form, &$form_state) {
+ $field_name = $this->options['id'];
+ foreach ($form_state['values'][$field_name] as $row_id => $value) {
+ if ($value == 'Drupal') {
+ form_set_error($field_name . '][' . $row_id, "You can't be named Drupal. That's my name.");
+ }
+ }
+ }
+
+ // Optional submit function.
+ function views_form_submit($form, &$form_state) {
+ // Do something here
+ }
+
+
+Modules can implement hook_views_form_validate($form, &$form_state) and hook_views_form_submit($form, &$form_state).
+
+The form is multistep by default, with one step: 'views_form_views_form'.
+A "form_example" module could add a confirmation step by setting:
+
+ $form_state['storage']['step'] = 'form_example_confirmation';
+
+in form_example_views_form_submit().
+Then, views_form would call form_example_confirmation($form, $form_state, $view, $output) to get that step.
+
+Relevant Views functions
+
+- template_preprocess_views_view()
+- views_form()
+- views_form_validate()
+- views_form_submit()
+- views_form_views_form()
+- views_form_views_form_validate()
+- views_form_views_form_submit()
+- theme_views_form_views_form()
+
+
+Hooks
+
+- hook_views_form_substitutions()
+- hook_views_form_validate($form, &$form_state)
+- hook_views_form_submit($form, &$form_state)
+
\ No newline at end of file
diff --git a/help/views.help.ini b/help/views.help.ini
index 428fd0d..fd35687 100644
--- a/help/views.help.ini
+++ b/help/views.help.ini
@@ -84,12 +84,12 @@ parent = display
[style-settings]
title = "Style settings"
-weight = 30
+weight = 30
[style]
title = "Style"
parent = style-settings
-weight = -20
+weight = -20
[style-grid]
title = "Grid (output style)"
@@ -118,7 +118,7 @@ weight = 50
[style-row]
title = "Row styles"
-weight = -10
+weight = -10
parent = style-settings
[style-jump]
@@ -156,7 +156,7 @@ parent = performance
[analyze-theme]
title = "Theme information"
parent = style-settings
-weight = 30
+weight = 30
[using-theme]
title = "Using Views templates"
@@ -166,7 +166,7 @@ weight = 40
[theme-css]
title = "Using CSS with Views"
parent = style-settings
-weight = 20
+weight = 20
[menu]
title = "Menu options (page display)"
@@ -182,11 +182,11 @@ weight = 45
[header]
title = "Header"
-weight = 50
+weight = 50
[footer]
title = "Footer"
-weight = 60
+weight = 60
[empty-text]
title = "Empty Text"
@@ -194,15 +194,15 @@ weight = 70
[field]
title = "Fields"
-weight = 80
+weight = 80
[relationship]
title = "Relationships"
-weight = 90
+weight = 90
[argument]
title = "Arguments"
-weight = 100
+weight = 100
[style-summary-unformatted]
title = "Summary Style: Unformatted (output style)"
@@ -214,7 +214,7 @@ parent = argument
[sort]
title = "Sort criteria"
-weight = 110
+weight = 110
[filter]
title = "Filters"
@@ -265,6 +265,11 @@ title = "How Views plugins work"
weight = -40
parent = api
+[api-forms]
+title = "Outputting form elements from handlers"
+weight = -30
+parent = api
+
[api-upgrading]
title = "Upgrading your module Views 1 to Views 2"
parent = api
diff --git a/theme/theme.inc b/theme/theme.inc
index ecb6729..3aaf728 100644
--- a/theme/theme.inc
+++ b/theme/theme.inc
@@ -144,6 +144,29 @@ function template_preprocess_views_view(&$vars) {
}
// Flatten the classes to a string for the template file.
$vars['classes'] = implode(' ', $vars['classes_array']);
+
+ // Check the fields on the View to see if any are adding form elements.
+ $has_form_fields = FALSE;
+ foreach ($view->field as $field_name => $field) {
+ if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
+ $has_form_fields = TRUE;
+ break;
+ }
+ }
+ // If form fields were found in the View, reformat the View output as a form.
+ if ($has_form_fields) {
+ $vars['rows'] = drupal_get_form(views_form_id($view), $view, $vars['rows']);
+ // The form is requesting that all non-essential views elements be hidden,
+ // usually because the rendered step is not a view result.
+ if ($view->show_view_elements == FALSE) {
+ $vars['header'] = '';
+ $vars['exposed'] = '';
+ $vars['pager'] = '';
+ $vars['footer'] = '';
+ $vars['more'] = '';
+ $vars['feed_icon'] = '';
+ }
+ }
}
/**
@@ -846,6 +869,39 @@ function template_preprocess_views_exposed_form(&$vars) {
$vars['button'] = drupal_render($form);
}
+/**
+ * Theme function for a View with form elements: replace the placeholders.
+ */
+function theme_views_form_views_form($form) {
+ $view = $form['view']['#value'];
+
+ // Placeholders and their substitutions (usually rendered form elements).
+ $search = array();
+ $replace = array();
+
+ // Prepare substitutions for views form elements.
+ foreach ($view->field as $field_name => $field) {
+ if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
+ foreach ($view->result as $row_id => $row) {
+ $search[] = '';
+ $replace[] = isset($form[$field_name][$row_id]) ? drupal_render($form[$field_name][$row_id]) : '';
+ }
+ }
+ }
+ // Add in substitutions from hook_views_form_substitutions().
+ $substitutions = module_invoke_all('views_form_substitutions');
+ foreach ($substitutions as $placeholder => $substitution) {
+ $search[] = $placeholder;
+ $replace[] = $substitution;
+ }
+
+ // Apply substitutions to the rendered output.
+ $form['output']['#value'] = str_replace($search, $replace, $form['output']['#value']);
+
+ // Render and add remaining form fields.
+ return drupal_render($form);
+}
+
function theme_views_mini_pager($tags = array(), $limit = 10, $element = 0, $parameters = array(), $quantity = 9) {
global $pager_page_array, $pager_total;
diff --git a/views.module b/views.module
index 97d7f39..7bbfd91 100644
--- a/views.module
+++ b/views.module
@@ -16,6 +16,37 @@ function views_api_version() {
}
/**
+ * Implements hook_forms().
+ *
+ * To provide distinct form IDs for Views forms, the View name and
+ * specific display name are appended to the base ID,
+ * views_form_views_form. When such a form is built or submitted, this
+ * function will return the proper callback function to use for the given form.
+ */
+function views_forms($form_id, $args) {
+ if (strpos($form_id, 'views_form_') === 0) {
+ return array(
+ $form_id => array(
+ 'callback' => 'views_form',
+ ),
+ );
+ }
+}
+
+/**
+ * Returns a form ID for a Views form using the name and display of the View.
+ */
+function views_form_id($view) {
+ $parts = array(
+ 'views_form',
+ $view->name,
+ $view->current_display,
+ );
+
+ return implode('_', $parts);
+}
+
+/**
* Views will not load plugins advertising a version older than this.
*/
function views_api_minimum_version() {
@@ -104,6 +135,8 @@ function views_theme($existing, $type, $theme, $path) {
}
}
+ $hooks['views_form_views_form'] = $base;
+
$hooks['views_exposed_form'] = $base + array(
'template' => 'views-exposed-form',
'pattern' => 'views_exposed_form__',
@@ -1100,6 +1135,132 @@ function vpr_trace() {
}
// ------------------------------------------------------------------
+// Views form (View with form elements)
+
+/**
+ * This is the entry function. Just gets the form for the current step.
+ * The form is always assumed to be multistep, even if it has only one
+ * step (the default 'views_form_vews_form' step). That way it is actually
+ * possible for modules to have a multistep form if they need to.
+ */
+function views_form(&$form_state, $view, $output) {
+ $form_state['storage']['step'] = isset($form_state['storage']['step']) ? $form_state['storage']['step'] : 'views_form_views_form';
+
+ $form = array();
+ $form['#validate'] = array('views_form_validate');
+ $form['#submit'] = array('views_form_submit');
+ // The view is stored in $form and not $form_state because
+ // the theme functions only get $form.
+ $form['view'] = array(
+ '#type' => 'value',
+ '#value' => $view,
+ );
+ // Tell the preprocessor whether it should hide the header, footer, pager...
+ $view->show_view_elements = ($form_state['storage']['step'] == 'views_form_views_form') ? TRUE : FALSE;
+
+ $form = $form_state['storage']['step']($form, $form_state, $view, $output);
+ return $form;
+}
+
+/**
+ * The basic form validate handler.
+ * Fires the hook_views_form_validate() function.
+ */
+function views_form_validate($form, &$form_state) {
+ // Fire the hook. Doesn't use module_invoke_all() because $form_state needs
+ // to be passed by reference.
+ foreach (module_implements('views_form_validate') as $module) {
+ $function = $module . '_views_form_validate';
+ $function($form, $form_state);
+ }
+}
+
+/**
+ * The basic form submit handler.
+ * Fires the hook_views_form_submit() function.
+ */
+function views_form_submit($form, &$form_state) {
+ // Fire the hook. Doesn't use module_invoke_all() because $form_state needs
+ // to be passed by reference.
+ foreach (module_implements('views_form_submit') as $module) {
+ $function = $module . '_views_form_submit';
+ $function($form, $form_state);
+ }
+}
+
+/**
+ * Callback for the main step of a Views form.
+ * Invoked by views_form().
+ */
+function views_form_views_form($form, &$form_state, $view, $output) {
+ $form['#prefix'] = '';
+ $form['#suffix'] = '
';
+ $form['#theme'] = 'views_form_views_form';
+ $form['#validate'][] = 'views_form_views_form_validate';
+ $form['#submit'][] = 'views_form_views_form_submit';
+
+ // Add the output markup to the form array so that it's included when the form
+ // array is passed to the theme function.
+ $form['output'] = array(
+ '#value' => $output,
+ // This way any additional form elements will go before the view
+ // (below the exposed widgets).
+ '#weight' => 50,
+ );
+
+ foreach ($view->field as $field_name => $field) {
+ // If the field provides a views form, allow it to modify the $form array.
+ if (property_exists($field, 'views_form_callback')) {
+ $callback = $field->views_form_callback;
+ $callback($view, $field, $form, $form_state);
+ }
+ elseif (method_exists($field, 'views_form')) {
+ $field->views_form($form, $form_state);
+ }
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#weight' => 100,
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for the first step of the views form.
+ * Calls any existing views_form_validate functions located
+ * on the views fields.
+ */
+function views_form_views_form_validate($form, &$form_state) {
+ $view = $form['view']['#value'];
+
+ // Call the validation method on every field handler that has it.
+ foreach ($view->field as $field_name => $field) {
+ if (method_exists($field, 'views_form_validate')) {
+ $field->views_form_validate($form, $form_state);
+ }
+ }
+}
+
+/**
+ * Submit handler for the first step of the views form.
+ * Calls any existing views_form_submit functions located
+ * on the views fields.
+ */
+function views_form_views_form_submit($form, &$form_state) {
+ $view = $form['view']['#value'];
+
+ // Call the submit method on every field handler that has it.
+ foreach ($view->field as $field_name => $field) {
+ if (method_exists($field, 'views_form_submit')) {
+ $field->views_form_submit($form, $form_state);
+ }
+ }
+}
+
+// ------------------------------------------------------------------
// Exposed widgets form
/**