Introdução

Esse artigo irá mostrar como construir um formulário customizado (custom form) em Drupal 8. Basicamente precisamos de duas coisas para criar uma custom form:

  1. Definir a rota (endereço) pela qual o custom form será acessado (Passo 1)
  2. Escrever a classe que irá lidar com o custom form (Passos 2, 3, 4, 5, 6)

Pré-requisitos

Irei assumir que já temos um módulo customizado chamado my_custom_module. Você pode consultar esse artigo Como criar um módulo customizado em Drupal 8 caso precise de ajuda para criar um módulo.

Todos os passos abaixo foram testados no Drupal 8.8.0.

Passo 1 - Definir a rota (url do formulário)

Para criar uma nova rota, precisamos primeiro criar um arquivo chamado my_custom_module.routing.yml dentro da pasta my_custome_module.

// Entre na pasta my_custom_module.
cd web/modules/custom/my_custom_module

// Crie um arquivo chamado my_custom_module.routing.yml.
touch my_custom_module.routing.yml

Em seguida, edite o arquivo que acabamos de criar da seguinte forma:

my_custom_module.simple_custom_module:
  path: '/simple-custom-form'
  defaults:
    _form: '\Drupal\my_custom_module\Form\SimpleCustomForm'
  requirements:
    _access: 'TRUE'

A primeira linha define o nome da rota (route id) e pode ser livremente definido (contanto que não exita outra rota com o mesmo nome).

A propriedade path define o endereço (url) dessa rota.

A propriedade _form define a classe que irá lidar com esse custom form.

Finalmente, a propriedade _access: 'true' define que essa rota será acessível a qualquer usuário (registrado ou anônimo).

Passo 2- Criar a classe do formulário

Crie o arquivo  my_custom_module\src\Form\SimpleCustomForm.php com o seguinte conteúdo:

<?php

namespace Drupal\my_custom_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SimpleCustomForm extends FormBase {
  public function getFormId() {
    // Define o id desse formulário.
    return 'simple_custom_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    // Aqui iremos construir a interface visual do formulário.
  }

  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Aqui iremos fazer qualquer tipo de validação necessária do formulário
    // enviado pelo usuário antes de proceder ao passo seguinte.
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Aqui iremos processar o formulário enviado pelo usuário.
  }

}

Essa classe possui 4 funções essenciais:

  1. buildForm(): constrói o formulário que será exibido ao usuário.
  2. validateForm(): quando o usuário envia um formulário, essa função irá fazer a validação do envio (por exemplo, verificar que o campo de senha possúi pelo menos 8 caracteres)
  3. submitForm(): esse função é executada depois da validateForm() validar os dados.

Agora continuaremos construindo cada função dessa classe nos passo 3, 4, 5 e 6. Caso você tenha pressa em ver o produto final, vá para a parte de Conclusão ao final dessa página.

Passo 3 - Construção da função buildForm()

Vamos adicionar diferentes tipos de campos nesse formulário como exemplo. A lista complete de tipos de campos disponíveis pode ser econtrada no site oficial do Drupal, lembrando que tipos de campos adicionais podem ser instalados por módulos adicionais.

public function buildForm(array $form, FormStateInterface $form_state, $username = NULL) {
  // Campo do tipo texto.
  $form['custom_text_field'] = [
    '#type' => 'textfield',
    '#title' => 'Text field:',
    '#required' => TRUE,
  ];

  // Campo do tipo número.
  $form['custom_number_field'] = [
    '#type' => 'number',
    '#title' => 'Number:',
  ];

  // Campo do tipo senha.
  $form['custom_password_field'] = [
    '#type' => 'password',
    '#title' => 'Password',
    '#size' => 25,
  ];

  // Campo do tipo data.
  $form['custom_date_field'] = [
    '#type' => 'date',
    '#title' => 'Date:',
  ];

  // Campo do tipo radio buttons.
  $form['custom_radio_field'] = [
    '#type' => 'radios',
    '#title' => 'Radio buttons:',
    '#options' => [
      'apple' => 'Apple',
      'banana' => 'Banana',
      'orange' => 'Orange',
    ],
  ];

  return $form;
}

The custom_text_field field has a '#required' => TRUE property, meaning that this field will be showing as required and will be enforced by the browser to have some value before submitting it. The '#required' => TRUE can be added to other form fields.

A clever user can trick this enforcement by the browser and submit a form with an empty required field. The next step shows how to add validation also on the server side so we dont' allow users to bypass required fields.

Step 4 - Build the form validation function

It's time to build the validateForm() function. In this example, we will enforce the submitted text value to be at least 4 characters long and the number field to have a maximum value of 100.

public function validateForm(array &$form, FormStateInterface $form_state) {
  // Limit text length to 4.
  $textfield_value = $form_state->getValue('custom_text_field');
  if (strlen($textfield_value) < 4) {
    $form_state->setErrorByName('custom_text_field', 'The text input must have at least 4 characters.');
  }

  // Limit maximum number value to 100.
  $number_value = $form_state->getValue('custom_number_field');
  if ($fnumber_value > 100) {
    $form_state->setErrorByName('custom_number_field', 'Then number value cannot be greater than 100.');
  }
}

Visit the form on the browser and try to submit a form with a text length less than 4 and(or) a number greater than 100. You should see an error message showing that the form values need to be fixed.

Step 5 - Build the form submission function

And finally the submitForm() function. Here we are going to simply show all the submitted values as status message on the website.

public function submitForm(array &$form, FormStateInterface $form_state) {
  // Show all form values as status message.
  foreach ($form_state->getValues() as $key => $value) {
    \Drupal::messenger()->addStatus($key . ': ' . $value);
  }
}

Try submitting a form and you should see all the form values showing up as status message.

Step 6 - Set redirection (optional)

After a form submission the user gets redirected to the same form page by default. In most of the cases this is not what we want.

In submitForm() you can set the redirect address with either $form_state->setRedirect() or $form_set->setRedirectUrl(). Check the documentation for FormStateInterface::setRediret and FormStateInterface::setRedirectUrl for more information on how to use them.

public function submitForm(array &$form, FormStateInterface $form_state) {
  // Show all form values as status message.
  foreach ($form_state->getValues() as $key => $value) {
    \Drupal::messenger()->addStatus($key . ': ' . $value);
  }

  // Redirect user to the front page.
  $form_state->setRedirect('<front>');
}

Conclusion

The SimpleCustomForm class in this example should look like this in the end:

<?php

namespace Drupal\my_custom_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SimpleCustomForm extends FormBase {
  public function getFormId() {
    // Here we set a unique form id
    return 'simple_custom_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state, $username = NULL) {
    // Textfield form element
    $form['custom_text_field'] = [
      '#type' => 'textfield',
      '#title' => 'Text field:',
      '#required' => TRUE,
    ];

    // Number field form element
    $form['custom_number_field'] = [
      '#type' => 'number',
      '#title' => 'Number:',
    ];

    // Password form element
    $form['custom_password_field'] = [
      '#type' => 'password',
      '#title' => 'Password',
      '#size' => 25,
    ];

    // Date form element
    $form['custom_date_field'] = [
      '#type' => 'date',
      '#title' => 'Date:',
    ];

    // Radio buttons form element
    $form['custom_radio_field'] = [
      '#type' => 'radios',
      '#title' => 'Radio buttons:',
      '#options' => [
        'apple' => 'Apple',
        'banana' => 'Banana',
        'orange' => 'Orange',
      ],
    ];
  
    return $form;
  }

  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Limit text length to 4.
    $textfield_value = $form_state->getValue('custom_text_field');
    if (strlen($textfield_value) < 4) {
      $form_state->setErrorByName('custom_text_field', 'The text input must have at least 4 characters.');
    }

    // Limit maximum number value to 100.
    $number_value = $form_state->getValue('custom_number_field');
    if ($number_value > 100) {
      $form_state->setErrorByName('custom_number_field', 'Then number value cannot be greater than 100.');
    }
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    foreach ($form_state->getValues() as $key => $value) {
      \Drupal::messenger()->addStatus($key . ': ' . $value);
    }

    $form_state->setRedirect('<front>');
  }

}

https://www.drupal.org/docs/8/api/form-api/conditional-form-fields