Formulaires multi-étapes / assistant

10

J'essaie de créer un formulaire multi-étapes / assistant pour Drupal 8.

  • L'utilisateur remplit les champs prénom, nom
  • Clique sur le bouton suivant
  • Remplit plus d'informations
  • Clique sur le bouton soumettre

Il existe actuellement de nombreuses ressources consacrées aux formulaires à plusieurs étapes ou à l'assistant pour Drupal 7 comme celui- ci et celui-ci .

D'un autre côté, j'ai eu du mal à comprendre quelle est la façon "Drupal" de créer des formulaires Drupal 8 multi-étapes / assistant.

J'ai fait quelques recherches et j'ai pensé qu'il y avait plusieurs approches:

Ces approches sont-elles valables pour Drupal 8?

chrisjlee
la source

Réponses:

12

La façon la plus simple de le faire est d'utiliser $ form_state. Dans votre méthode formBuild (), vous avez un if / else ou un commutateur basé sur quelque chose comme $form_state['step']et affiche différents éléments de formulaire. Ensuite, vous avez la même chose dans votre rappel de soumission ou vous avez plusieurs rappels de soumission, qui font quelque chose à un objet dans $ form_state que vous construisez, modifiez l'étape et définissez l' $form_state['rebuild']indicateur sur TRUE.

Il y a quelques inconvénients à cette approche, c'est pourquoi (entre autres raisons) l'assistant de formulaire ctools a été créé. Cela peut devenir compliqué si vous avez plusieurs étapes et devez définir tout cela dans une seule fonction / classe de formulaire et tout se passe dans les requêtes POST.

L'assistant de formulaire ctools regroupe plusieurs formulaires séparés et contrôle la navigation de l'un à l'autre. Vous utilisez également le cache d'objets ctools pour stocker votre objet au lieu de $ form_state, car il n'est plus partagé entre vos formulaires.

Bien que ce système n'existe pas encore, le cache d'objets ctools a été porté sur 8.x et s'appelle désormais user tempstore, disponible en tant que service: \Drupal::service('user.private_tempstore')(avant 8.0-beta8 appelé user.tempstore). Il s'agit d'une couche au-dessus du magasin de valeurs clés expirable qui introduit la propriété des données stockées. C'est donc ce qui alimente le message bien connu dans les vues qu'un autre utilisateur modifie actuellement cette vue et qu'il est verrouillé pour cette raison. Un autre avantage par rapport à l'utilisation de $ _SESSION est que vos données ne doivent être chargées qu'en cas de besoin, lorsque vous modifiez 3 vues, puis utiliser $ _SESSION signifie que vous devez les charger et les transporter à chaque demande de page.

Si vous n'en avez pas besoin, vous pouvez vous fier à la session ou la placer directement dans un magasin de valeurs de clés expirable ($ form_state y est également stocké maintenant, pas un pseudo-cache comme c'était le cas dans 7.x).

Le système de configuration n'est cependant pas un bon match. Ce n'est pas destiné au contenu par utilisateur (ou au contenu du tout), car il n'est pas vraiment évolutif pour stocker des milliers ou des dizaines de milliers d'enregistrements et peut faire des hypothèses pour précharger tout ce dont il pourrait avoir besoin sur une demande de page donnée ( pas encore, mais il y a un problème pour y arriver)

Berdir
la source
Encore une question sur votre réponse. Cela peut être une question stupide: \ Drupal :: service ('user.tempstore') est-il également disponible pour les utilisateurs anonymes?
chrisjlee
Oui, cela revient à l'ID de session pour les utilisateurs anormaux. Voir api.drupal.org/api/drupal/…
Berdir
4

Normalement, vous pouvez stocker des valeurs de formulaire entre les étapes à l'aide du cache d'objets cTools (similaire aux formulaires à plusieurs étapes dans Drupal 7 ), ou via le $form_state(comme dans ce tutoriel ).

Dans Drupal 8, vous pouvez hériter de la FormBaseclasse pour créer une nouvelle classe à plusieurs étapes.

Dans l'article Comment créer des formulaires en plusieurs étapes dans Drupal 8, vous pouvez trouver un moyen simple de créer un formulaire en plusieurs étapes dans Drupal 8.

Tout d'abord, vous devrez créer la classe de base qui sera chargée d'injecter les dépendances nécessaires.

Nous regrouperons toutes les classes de formulaires et les placerons dans un nouveau dossier appelé Multistepsitué dans le Formrépertoire des plugins de notre module de démonstration. Ceci est purement pour avoir une structure propre et pouvoir dire rapidement quelles formes font partie de notre processus de formulaire en plusieurs étapes.

Voici le code de démonstration (pour le MultistepFormBase.phpfichier):

/**
 * @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);
    }
  }
}

Ensuite, vous pouvez créer la classe de formulaires réelle dans un fichier appelé 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');
  }
}

Dans la buildForm()méthode, nous définissons nos deux éléments de formulaire fictifs. Notez que nous récupérons d'abord la définition de formulaire existante dans la classe parent. Les valeurs par défaut de ces champs sont définies comme les valeurs trouvées dans le magasin pour ces clés (afin que les utilisateurs puissent voir les valeurs qu'ils ont remplies à cette étape s'ils y reviennent). Enfin, nous modifions la valeur du bouton d'action sur Suivant (pour indiquer que ce formulaire n'est pas le dernier).

Dans la submitForm()méthode, nous enregistrons les valeurs soumises dans le magasin, puis redirige vers le deuxième formulaire (qui peut être trouvé sur l'itinéraire demo.multistep_two). Gardez à l'esprit que nous ne faisons aucune sorte de validation ici pour garder le code léger. Mais la plupart des cas d'utilisation exigeront une certaine validation d'entrée.

Et mettez à jour votre fichier de routage dans le module de démonstration ( 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'

Enfin, créez le second formulaire ( 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');
  }
}

À l'intérieur de la submitForm()méthode, nous enregistrons à nouveau les valeurs dans le magasin et nous reportons à la classe parente pour conserver ces données de la manière qu'elle juge appropriée. Nous redirigeons ensuite vers la page que nous voulons (l'itinéraire que nous utilisons ici est factice).

Nous devrions maintenant avoir un formulaire en plusieurs étapes qui utilise le PrivateTempStorepour garder les données disponibles sur plusieurs demandes. Si nous avons besoin de plus d'étapes, tout ce que nous avons à faire est de créer d'autres formulaires, de les ajouter entre les formulaires existants et de faire quelques ajustements.

Kenorb
la source
1

L' assistant à plusieurs étapes que vous avez mentionné, il est déjà intégré à CTools, voir: Prise en charge de l'assistant pour 8.x-3.x , vous pouvez donc envisager de l'étendre your_module.services.yml, par exemple

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

puis étendez la classe 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
la source
connaissez-vous un exemple qui pourrait aider à comprendre comment utiliser l'assistant multi-étapes CTools?
Duncanmoo
1
@Duncanmoo Je n'ai pas, mais n'hésitez pas à poser une autre question avec un problème spécifique que vous rencontrez, ou regardez dans des Tests/Wizard/CToolsWizard*fichiers où vous pouvez trouver des tests (par exemple testWizardSteps).
kenorb