diff --git a/docs/views.api.php b/docs/views.api.php index b2bcc62..6725627 100644 --- a/docs/views.api.php +++ b/docs/views.api.php @@ -555,6 +555,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['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['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..887501d --- /dev/null +++ b/help/api-forms.html @@ -0,0 +1,78 @@ +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(), and the real form with matching structure in views_form(). +When the View is being preprocessed for the theme file, all placeholders get replaced with the rendered form elements. + +Note that views_form() gets the whole form, and needs to return the whole form. + +The views handler can also implement views_form_validate() and views_form_submit(). +
+  function render($values) {
+    return '';
+  }
+
+  function views_form($form, $form_state) {
+    // The view is empty, abort.
+    if (empty($this->view->result)) {
+      return $form;
+    }
+
+    $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' => '',
+      );
+    }
+    return $form;
+  }
+
+  // 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 == 'Bojan') {
+        form_set_error($field_name . '][' . $row_id, "You can't be named Bojan. That's my name.");
+      }
+    }
+  }
+
+  // Optional submit function.
+  function views_form_submit($form, $form_state) {
+    // Do something here
+  }
+
+ +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['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

+ + +

Hooks

+ \ No newline at end of file diff --git a/help/views.help.ini b/help/views.help.ini index 12d30f5..83b98a9 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 = "Output styles (View styles)" 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 [semantic-views] title = "Semantic Views" @@ -186,11 +186,11 @@ weight = 45 [header] title = "Header" -weight = 50 +weight = 50 [footer] title = "Footer" -weight = 60 +weight = 60 [empty-text] title = "Empty Text" @@ -198,15 +198,15 @@ weight = 70 [field] title = "Fields" -weight = 80 +weight = 80 [relationship] title = "Relationships" -weight = 90 +weight = 90 [argument] title = "Arguments/Contextual Filters" -weight = 100 +weight = 100 [style-summary-unformatted] title = "Summary Style: Unformatted (output style)" @@ -218,7 +218,7 @@ parent = argument [sort] title = "Sort criteria" -weight = 110 +weight = 110 [filter] title = "Filters" @@ -268,6 +268,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 to Drupal 7 (API)" parent = api diff --git a/theme/theme.inc b/theme/theme.inc index b481cf7..bf772b2 100644 --- a/theme/theme.inc +++ b/theme/theme.inc @@ -140,6 +140,30 @@ 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) { + $form = 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 ($form['show_view_elements']['#value'] == FALSE) { + $vars['header'] = ''; + $vars['exposed'] = ''; + $vars['pager'] = ''; + $vars['footer'] = ''; + $vars['more'] = ''; + $vars['feed_icon'] = ''; + } + $vars['rows'] = drupal_render($form); + } } /** @@ -844,6 +868,40 @@ function template_preprocess_views_exposed_form(&$vars) { $vars['button'] = drupal_render_children($form); } +/** + * Theme function for a View with form elements: replace the placeholders. + */ +function theme_views_form_views_form($variables) { + $form = $variables['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']['#markup'] = str_replace($search, $replace, $form['output']['#markup']); + + // Render and add remaining form fields. + return drupal_render_children($form); +} + function theme_views_mini_pager($vars) { global $pager_page_array, $pager_total; diff --git a/views.module b/views.module index 355a464..798c514 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() { @@ -100,6 +131,10 @@ function views_theme($existing, $type, $theme, $path) { } } + $hooks['views_form_views_form'] = $base + array( + 'render element' => 'form', + ); + $hooks['views_exposed_form'] = $base + array( 'template' => 'views-exposed-form', 'pattern' => 'views_exposed_form__', @@ -1437,6 +1472,136 @@ 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, &$form_state, $view, $output) { + $form_state['step'] = isset($form_state['step']) ? $form_state['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... + $form['show_view_elements'] = array( + '#type' => 'value', + '#value' => ($form_state['step'] == 'views_form_views_form') ? TRUE : FALSE, + ); + + $form = $form_state['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) { + 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) { + foreach (module_implements('views_form_submit') as $module) { + $function = $module . '_views_form_submit'; + $function($form, $form_state); + } +} + +/** + * This function was sponsored by the Department of Redundancy Department. + */ +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( + '#type' => 'markup', + '#markup' => $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) { + // Retrieve the form array for this item from the handler. + if (property_exists($field, 'views_form_callback')) { + $callback = $field->views_form_callback; + $form = $callback($view, $field, $form, $form_state); + } + elseif (method_exists($field, 'views_form')) { + $form = $field->views_form($form, $form_state); + } + } + + $form['actions'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('form-actions')), + '#weight' => 100, + ); + + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + 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 /**