Formularios de múltiples pasos / asistente

10

Estoy tratando de crear un formulario de múltiples pasos / asistente para Drupal 8.

  • El usuario completa los campos de nombre y apellido
  • Clics en el siguiente botón
  • Llena más información
  • Clics en el botón enviar

En este momento hay muchos recursos dedicados a varios pasos o formas del asistente para Drupal 7 como esta uno y este .

Por otro lado, he tenido algunos problemas para descubrir cuál es la forma "Drupal" de crear formularios de múltiples pasos / asistente de Drupal 8.

Investigué un poco y pensé que hay varios enfoques:

¿Son esos enfoques válidos para Drupal 8?

chrisjlee
fuente

Respuestas:

12

La forma más fácil de hacer esto es usar $ form_state. En su método formBuild (), tiene un if / else o switch basado en algo como $form_state['step']y muestra diferentes elementos de formulario. Luego, tiene lo mismo en su devolución de llamada de envío o tiene varias devoluciones de llamada de envío, que hacen algo a un objeto en $ form_state que está construyendo, cambie el paso y establezca el $form_state['rebuild']indicador en VERDADERO.

Hay algunas desventajas de ese enfoque, por lo que (entre otras razones) se creó el asistente de formularios ctools. Puede complicarse si tiene varios pasos y tiene que definir todo eso en una sola función / clase de formulario y todo sucede en las solicitudes POST.

Lo que hace el asistente de formularios de ctools es agrupar múltiples formularios separados y controlar la navegación de uno a otro. También usa el caché de objetos de ctools para almacenar su objeto en lugar de $ form_state, porque eso ya no se comparte entre sus formularios.

Mientras que el sistema no existe, sin embargo, los ctools objeto de caché se ha portado a 8.x y ahora se llama tempstore usuario, disponible como un servicio: \Drupal::service('user.private_tempstore')(antes 8,0-Beta8 llama user.tempstore). Esta es una capa en la parte superior del almacén de valores de clave expirables que introduce la propiedad de los datos almacenados allí. Entonces, esto es lo que impulsa el conocido mensaje en las vistas de que un usuario diferente está editando esa vista y está bloqueado por esa razón. Otra ventaja sobre el uso de $ _SESSION para eso es que sus datos solo deben cargarse cuando sea necesario, cuando está editando 3 vistas, luego usar $ _SESSION significaría que debe cargarlos y transportarlos en cada solicitud de página.

Si no necesita eso, puede confiar en la sesión o también ponerla directamente en un almacén de valores de clave expirable ($ form_state también se almacena allí ahora, no un pseudo-caché como estaba en 7.x).

Sin embargo, el sistema de configuración no es una buena combinación. Eso no está destinado al contenido por usuario (o al contenido), ya que en realidad no se escala para almacenar miles o decenas de miles de registros y puede hacer algunas suposiciones para precargar todo lo que pueda necesitar en una solicitud de página determinada ( todavía no, pero hay un problema para que eso suceda)

Berdir
fuente
Una pregunta más sobre tu respuesta. Esta podría ser una pregunta tonta: ¿\ Drupal :: service ('user.tempstore') también está disponible para usuarios anónimos?
chrisjlee
Sí, recurre a la identificación de sesión para usuarios anónimos. Ver api.drupal.org/api/drupal/…
Berdir
4

Normalmente, puede almacenar valores de formulario entre los pasos utilizando el caché de objetos de cTools (similar a los formularios de varios pasos en Drupal 7 ), o mediante $form_state(según este tutorial ).

En Drupal 8 puede heredar la FormBaseclase para crear una nueva clase de varios pasos.

En el artículo Cómo crear formularios de varios pasos en Drupal 8 , puede encontrar una manera sencilla de crear un formulario de varios pasos en Drupal 8.

En primer lugar, necesitaría crear la clase base que se encargará de inyectar las dependencias necesarias.

Agruparemos todas las clases de formulario y las colocaremos dentro de una nueva carpeta llamada Multistepubicada dentro del Formdirectorio de complementos de nuestro módulo de demostración. Esto es puramente por tener una estructura limpia y poder determinar rápidamente qué formularios son parte de nuestro proceso de formularios de varios pasos.

Aquí está el código de demostración (para el MultistepFormBase.phparchivo):

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepFormBase.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;

abstract class MultistepFormBase extends FormBase {

  /**
   * @var \Drupal\user\PrivateTempStoreFactory
   */
  protected $tempStoreFactory;

  /**
   * @var \Drupal\Core\Session\SessionManagerInterface
   */
  private $sessionManager;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
  private $currentUser;

  /**
   * @var \Drupal\user\PrivateTempStore
   */
  protected $store;

  /**
   * Constructs a \Drupal\demo\Form\Multistep\MultistepFormBase.
   *
   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
   * @param \Drupal\Core\Session\AccountInterface $current_user
   */
  public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, AccountInterface $current_user) {
    $this->tempStoreFactory = $temp_store_factory;
    $this->sessionManager = $session_manager;
    $this->currentUser = $current_user;

    $this->store = $this->tempStoreFactory->get('multistep_data');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('user.private_tempstore'),
      $container->get('session_manager'),
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Start a manual session for anonymous users.
    if ($this->currentUser->isAnonymous() && !isset($_SESSION['multistep_form_holds_session'])) {
      $_SESSION['multistep_form_holds_session'] = true;
      $this->sessionManager->start();
    }

    $form = array();
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
      '#button_type' => 'primary',
      '#weight' => 10,
    );

    return $form;
  }

  /**
   * Saves the data from the multistep form.
   */
  protected function saveData() {
    // Logic for saving data goes here...
    $this->deleteStore();
    drupal_set_message($this->t('The form has been saved.'));

  }

  /**
   * Helper method that removes all the keys from the store collection used for
   * the multistep form.
   */
  protected function deleteStore() {
    $keys = ['name', 'email', 'age', 'location'];
    foreach ($keys as $key) {
      $this->store->delete($key);
    }
  }
}

Luego puede crear la clase de formularios real dentro de un archivo llamado MultistepOneForm.php:

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepOneForm.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormStateInterface;

class MultistepOneForm extends MultistepFormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'multistep_form_one';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['name'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
      '#default_value' => $this->store->get('name') ? $this->store->get('name') : '',
    );

    $form['email'] = array(
      '#type' => 'email',
      '#title' => $this->t('Your email address'),
      '#default_value' => $this->store->get('email') ? $this->store->get('email') : '',
    );

    $form['actions']['submit']['#value'] = $this->t('Next');
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->store->set('email', $form_state->getValue('email'));
    $this->store->set('name', $form_state->getValue('name'));
    $form_state->setRedirect('demo.multistep_two');
  }
}

En el buildForm()método estamos definiendo nuestros dos elementos de forma ficticia. Tenga en cuenta que estamos recuperando la definición de formulario existente de la clase principal primero. Los valores predeterminados para estos campos se establecen como los valores encontrados en la tienda para esas claves (de modo que los usuarios puedan ver los valores que completaron en este paso si vuelven a ello). Finalmente, estamos cambiando el valor del botón de acción a Siguiente (para indicar que este formulario no es el último).

En el submitForm()método guardamos los valores enviados a la tienda y luego redirigimos al segundo formulario (que se puede encontrar en la ruta demo.multistep_two). Tenga en cuenta que no estamos haciendo ningún tipo de validación aquí para mantener el código ligero. Pero la mayoría de los casos de uso requerirán alguna validación de entrada.

Y actualice su archivo de enrutamiento en el módulo de demostración ( demo.routing.yml):

demo.multistep_one:
  path: '/demo/multistep-one'
  defaults:
    _form: '\Drupal\demo\Form\Multistep\MultistepOneForm'
    _title: 'First form'
  requirements:
    _permission: 'access content'
demo.multistep_two:
  path: '/demo/multistep-two'
  defaults:
    _form: '\Drupal\demo\Form\Multistep\MultistepTwoForm'
    _title: 'Second form'
  requirements:
    _permission: 'access content'

Finalmente, cree la segunda forma ( MultistepTwoForm):

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepTwoForm.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

class MultistepTwoForm extends MultistepFormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'multistep_form_two';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['age'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your age'),
      '#default_value' => $this->store->get('age') ? $this->store->get('age') : '',
    );

    $form['location'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your location'),
      '#default_value' => $this->store->get('location') ? $this->store->get('location') : '',
    );

    $form['actions']['previous'] = array(
      '#type' => 'link',
      '#title' => $this->t('Previous'),
      '#attributes' => array(
        'class' => array('button'),
      ),
      '#weight' => 0,
      '#url' => Url::fromRoute('demo.multistep_one'),
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->store->set('age', $form_state->getValue('age'));
    $this->store->set('location', $form_state->getValue('location'));

    // Save the data
    parent::saveData();
    $form_state->setRedirect('some_route');
  }
}

Dentro del submitForm()método, guardamos nuevamente los valores en la tienda y los diferimos a la clase padre para conservar estos datos de la forma que mejor le parezca. Luego redirigimos a la página que queramos (la ruta que usamos aquí es falsa).

Ahora deberíamos tener un formulario de varios pasos en funcionamiento que utilice el PrivateTempStorepara mantener los datos disponibles en múltiples solicitudes. Si necesitamos más pasos, todo lo que tenemos que hacer es crear algunos formularios más, agregarlos entre los existentes y hacer un par de ajustes.

kenorb
fuente
1

El asistente de varios pasos que ha mencionado, ya está integrado con CTools, consulte: Soporte de asistente para 8.x-3.x , por lo que puede considerar ampliarlo your_module.services.yml, por ejemplo

services:
  ctools.wizard.form:
    class: Drupal\MyModuleMultistep\Controller\MyWizardForm

luego extienda la clase en src/Controller/MyWizardForm.php:

<?php

/**
 * @file
 * Contains \Drupal\MyModuleMultistep\Controller\MyWizardForm
 */

namespace Drupal\MyModuleMultistep\Controller;

/**
 * Wrapping controller for wizard forms that serve as the main page body.
 */
class MyWizardForm extends WizardFormController {

}
kenorb
fuente
¿Conoces un ejemplo que pueda ayudar a entender cómo usar el asistente multipaso CTools?
Duncanmoo
1
@Duncanmoo No tengo, pero siéntase libre de hacer otra pregunta con un problema específico que tenga, o buscar en los Tests/Wizard/CToolsWizard*archivos donde puede encontrar algunas pruebas (por ejemplo testWizardSteps).
kenorb