• David Duymelinck

  • Software Engineer

Customising a form in Drupal 8 is drastically different from Drupal 7. You will have to use services, twig templates and yaml files rather than hooks. In this article I will demonstrate how to change the functionality and the style of an existing form.

Where to start?

In Drupal 8 a form is a class and several base classes are provided in the open source core. FormBase is the most generic one; more specific form bases are ConfigFormBase, ConfirmFormBase, and EntityForm. You can build new forms for your application directly from these bases, but you can also start from one of the standard forms that are provided by Drupal 8. In this post I will explain how to create a custom form by making adaptations to an existing form. 

Form functionality and structure

I want to create a new form by making changes to the functionality or to the structure of an existing form. As an example, I will explain how to add a new field (email) to UserLoginForm, a standard form that is part of the Drupal core. 

EventSubscriber

The first step is to write a service that intercepts the original UserLoginForm and directs to the adapted form. The most elegant way to do this is to use an EventSubscriber that changes the _form property of the route. I have added the following service to my module’s services.yml:

modules/custom/form_overwrite/form_overwrite.services.yml

services:
    route_subscriber:
        class: Drupal\form_overwrite\Routing\RouteSubscriber
        tags: 
 - {name: event_subscriber }

By using the tag event_subscriber it is defined that the service will be triggered by an event.

The RouteSubscriber that I am creating uses the RouteSubscriberBase from the Drupal core, to limit the amount of code.

modules/custom/form_overwrite/src/Routing/RouteSubscriber.php

/**
 * @file
 * Contains \Drupal\form_overwrite\Routing\RouteSubscriber.
 */

namespace Drupal\form_overwrite\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
 * Listens to the dynamic route events.
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
     if ($route = $collection->get('user.login')) {
$route->setDefault('_form', '\Drupal\form_overwrite\Form\NewUserLoginForm');
     }

   }
}

In the above code I have changed the route to return the adapted NewUserLoginForm instead of the original UserLoginForm. The get method returns the configuration of the route.

It is beyond the scope of this blog, but it is worth mentioning that services like the RouteSubscriber are also used in Drupal 8 to change page titles, permissions, etcetera.

NewUserLoginForm

Now I can start making adaptations to UserLoginForm and create NewUserLoginForm. This is shown in below code.

modules/custom/form_overwrite/src/Form/NewUserLoginForm.php

/**
 * @file
 * Contains \Drupal\form_overwrite\Form\NewUserLoginForm.
 */

namespace Drupal\form_overwrite\Form;

use Drupal\Core\Form\FormStateInterface;
use Drupal\user\Form\UserLoginForm;

/**
 * Provides a user login form.
 */
class NewUserLoginForm extends UserLoginForm {
    public function buildForm(array $form, FormStateInterface $form_state) {
         $form = parent::buildForm($form, $form_state);
         $form['email'] = [
'#type' => 'email',
'#default_value' => ‘’
          ];
         return $form;
      }
}

In the code above I have added a new field to the form: email. You can use the same methodology to make other adaptations to the structure and functionality of an existing form, for instance to change the validation or adapt submit actions.

What I have demonstrated so far is how to create a new form based on an existing form, how to route this form correctly and how to change its structure and functionality. In the remainder of this blog I will explain how to alter the style of an existing form. 

Look & feel of the form

The style of a form is not dealt with so much in code, but is taken care of by themes. In Drupal 8 there are 2 straightforward ways of changing the look & feel of an existing form

  • First I will explain how to add markup around the form by using ‘suggestions’. 
  • In the second part I will detail how to change markup between the form elements.

Form theming with suggestions

Here is how you can use suggestions to adapt the markup around the form

1. Go to the services.yml in the sites/default directory and go to the twig.config section

2. Set debug on ‘true’ and the suggestions will be shown in html comments. The suggestions show you which template names you can use to change the markup. 

3. Then add your own suggestions by using the code below:

modules/custom/form_overwrite/form_overwrite.module

/**
 * Implements hook_theme_suggestions_alter
 */
function form_overwrite_theme_suggestions_alter(array &$suggestions, array $variables)
{

    if (isset($variables['element']) && isset($variables['element']['#type']) && $variables['element']['#type'] == 'form') {
        $original_theme_hook = $variables['theme_hook_original'];

        $suggestions[] = $original_theme_hook . '__' . str_replace('-', '_', $variables['element']['#id']);
    }

    return $suggestions;
 
}

The code creates a hook_theme_suggestions_alter and puts it in my form_overwrite.module file. You could as well decide to place it in the .theme file of your custom theme

This hook offers a suggestion based on the form id. For the UserLoginForm that I have worked with in the first part of this blog, the suggestion will be form__user_login_form.html.twig. The template that I will create next has to have hyphens instead of underscores to be picked up by the suggestion.

4. create a template that will render based on the new suggestion. In below code I create a form that adds a div with a class around the form.

modules/custom/form_overwrite/templates/form__user_login_form.html.twig

{#
/**
 * @file
 * Theme override for a 'form' element.
 *
 * Available variables
 * - attributes: A list of HTML attributes for the wrapper element.
 * - children: The child elements of the form.
 *
 * @see template_preprocess_form()
 */
#}
<div class=”my-login-form”>
<form{{ attributes }}>
  {{ children }}
</form>
</div>

This approach based on using suggestions to change the markup around the form can be particularly useful if you have a css grid of your own or use one of the existing css grids/frameworks.

Markup between the form elements

If you want to add markup between the form elements you have to do as follows:

1 Create below hook:

modules/custom/form_overwrite/form_overwrite.module

/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function form_overwrite_form_user_login_form_alter(&$form, FormStateInterface &$form_state) {
  $form['#theme'] = ['my_user_login'];
}

 2 If you are familiar with Drupal theming you know that the #theme in above code calls a theme that is defined in the hook_theme hook

/**
 * Implements hook_theme()
 */
function form_overwrite_theme() {
    $themes[‘my_user_login’] = [
‘render element’ => ‘form’
    ];


   return $themes;
}

3a To make changes to the look and feel of the form, excluding the fields, create a my_user_login.html.twig file:

{{ form.name }}
{{ form.pass }}
{{ form.test }}
{{ form.form_build_id }} {# required #}
{{ form.form_id }} {# required #}
{{ form.actions }}

I didn’t add any markup in this example to give you a better idea of the fields that are needed for a form. If you want to make changes however, this is the place to add markup. In Drupal the form.actions field is in most cases the submit button.

3b To make changes to the fields in your form, you have to change the specific twig templates. First check which twig templates correspond with the base theme of your Drupal application. It is important that you give new twig templates the same name as the original ones, otherwise your new templates will not be recognised.

In below code I will demonstrate how to change the markup of the ‘submit’ button from Drupal style to Bootstrap style. This is done in the input--submit.html.twig file.

{%
set classes = [
   'btn',
   'btn-default'
 ]
%}
<input{{ attributes.addClass(classes) }} />

Conclusion

In the first part of this article I have shown how to change the functionality and structure of an existing form. In the second part I have demonstrated how to theme a form. Forms are manipulated in Drupal 8 predominantly by using services, twig templates and yaml files, rather than hooks. Hooks are still used, but the code in the hooks is limited in comparison with Drupal 7. Events on the other hand have become much more important.

Verwante Artikels