Formulários de várias etapas / assistente

10

Estou tentando criar um formulário de várias etapas / assistente para o Drupal 8.

  • O usuário preenche os campos de nome e sobrenome
  • Cliques no próximo botão
  • Preenche mais informações
  • Cliques no botão enviar

Atualmente muitos recursos dedicados a várias etapas ou formas assistente para Drupal 7 como esta um e este .

Por outro lado, tive alguns problemas para descobrir qual é a maneira "Drupal" de criar formulários de várias etapas / assistente do Drupal 8.

Eu fiz algumas pesquisas e percebi que existem várias abordagens:

Essas abordagens são válidas para o Drupal 8?

chrisjlee
fonte

Respostas:

12

A maneira mais fácil de fazer isso é usar $ form_state. No seu método formBuild (), você tem um if / else ou alterna com base em algo como $form_state['step']e exibe diferentes elementos de formulário. Então você tem o mesmo no seu retorno de chamada de envio ou vários retornos de chamada de envio, que fazem algo com um objeto em $ form_state que você está construindo, altere a etapa e defina o $form_state['rebuild']sinalizador como TRUE.

Existem algumas desvantagens nessa abordagem, e é por isso que (entre outros motivos) o assistente de formulário ctools foi criado. Pode ser complicado se você tiver várias etapas e precisar definir tudo isso em um único formulário de função / classe e tudo acontecer nas solicitações POST.

O que o assistente de formulário ctools faz é agrupar vários formulários separados e controlar a navegação de um para o outro. Você também usa o cache de objetos ctools para armazenar seu objeto em vez de $ form_state, porque isso não é mais compartilhado entre seus formulários.

Embora esse sistema ainda não exista, o cache do objeto ctools foi portado para 8.x e agora é chamado de tempstore do usuário, disponível como um serviço: \Drupal::service('user.private_tempstore')(antes da 8.0-beta8 chamada user.tempstore). Essa é uma camada no topo do armazenamento de valores-chave expirável que introduz a propriedade dos dados armazenados. Portanto, é isso que alimenta a mensagem conhecida nas visualizações de que um usuário diferente está editando essa visualização e está bloqueada por esse motivo. Outra vantagem sobre o uso de $ _SESSION é que seus dados só precisam ser carregados quando necessário, quando você estiver editando três visualizações, e usar $ _SESSION significaria que você teria que carregar e carregá-los em todas as solicitações de página.

Se você não precisar disso, poderá confiar na sessão ou também colocá-la diretamente em um armazenamento de valores-chave expirável ($ form_state também está armazenado lá agora, não em um pseudo-cache como no 7.x).

O sistema de configuração, no entanto, não é uma boa correspondência. Isso não se destina ao conteúdo por usuário (ou ao conteúdo), pois não é realmente dimensionável para armazenar milhares ou dezenas de milhares de registros e pode fazer algumas suposições para pré-carregar tudo o que é necessário em uma determinada solicitação de página ( ainda não, mas há um problema para fazer isso acontecer)

Berdir
fonte
Mais uma pergunta sobre sua resposta. Esta pode ser uma pergunta boba: \ Drupal :: service ('user.tempstore') também está disponível para usuários anônimos?
Chrisjlee #
Sim, ele volta ao ID da sessão para usuários anômalos. Veja api.drupal.org/api/drupal/…
Berdir
4

Normalmente, você pode armazenar valores de formulário entre as etapas, usando o cache de objeto do cTools (semelhante aos formulários de várias etapas do Drupal 7 ) ou através do $form_state(conforme este tutorial ).

No Drupal 8, você pode herdar a FormBaseclasse para criar uma nova classe de várias etapas.

No artigo Como criar formulários de várias etapas no Drupal 8, você pode encontrar uma maneira simples de criar um formulário de várias etapas no Drupal 8.

Primeiro, você precisaria criar a classe base que será responsável por injetar as dependências necessárias.

Agruparemos todas as classes de formulário e as colocaremos em uma nova pasta chamada Multisteplocalizada no Formdiretório de plugins do nosso módulo de demonstração. Isso é apenas para ter uma estrutura limpa e ser capaz de dizer rapidamente quais formulários fazem parte do nosso processo de formulários de várias etapas.

Aqui está o código de demonstração (para MultistepFormBase.phparquivo):

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

Em seguida, você pode criar a classe de formulários real dentro de um arquivo chamado 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');
  }
}

No buildForm()método, estamos definindo nossos dois elementos de forma fictícia. Observe que estamos recuperando a definição de formulário existente da classe pai primeiro. Os valores padrão para esses campos são definidos como os valores encontrados na loja para essas chaves (para que os usuários possam ver os valores preenchidos nesta etapa, se voltarem a ele). Por fim, estamos alterando o valor do botão de ação para Avançar (para indicar que esse formulário não é o final).

No submitForm()método, salvamos os valores enviados na loja e, em seguida, redirecionamos para o segundo formulário (que pode ser encontrado na rota demo.multistep_two). Lembre-se de que não estamos fazendo nenhum tipo de validação aqui para manter o código leve. Mas a maioria dos casos de uso exigirá alguma validação de entrada.

E atualize seu arquivo de roteamento no módulo de demonstração ( 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'

Por fim, crie o segundo formulário ( 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 do submitForm()método, salvamos novamente os valores na loja e adiamos para a classe pai para persistir esses dados da maneira que achar melhor. Em seguida, redirecionamos para a página que desejamos (a rota que usamos aqui é fictícia).

Agora devemos ter um formulário de várias etapas que use o PrivateTempStorepara manter os dados disponíveis em várias solicitações. Se precisarmos de mais etapas, tudo o que precisamos fazer é criar mais algumas formas, adicioná-las entre as existentes e fazer alguns ajustes.

kenorb
fonte
1

O assistente de várias etapas que você mencionou, já está integrado ao CTools, consulte: Assistente de Suporte para 8.x-3.x , para que você possa estendê-lo your_module.services.yml, por exemplo

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

estenda a classe em 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
fonte
você conhece um exemplo que possa ajudar a entender como usar o assistente de várias etapas do CTools?
Duncanmoo 07/07
1
@ Duncanmoo Eu não tenho, mas sinta-se à vontade para fazer outra pergunta com um problema específico que você está tendo ou procurar Tests/Wizard/CToolsWizard*arquivos onde você pode encontrar alguns testes (por exemplo testWizardSteps).
Kenorb 07/07