I needed to create theme templates for specific fields in specific forms, and Drupal's theme template structure made the task pretty straightforward. I'm using a name
text input field on a form named contact
as an example here. The goal was to provide theme templates that could target that specific field, and also for common field types shared within the specific contact
form.
Templates needed
Here are the template suggestions that I thought would be useful
input--textfield--contact.html.twig # all text fields in the `contact` form
input--name--contact.html.twig # The specific field `name` in the `contact` form
Here are the form field element values I needed to define the suggestions
Generic form element type | input |
---|---|
Specific Drupal API type | textfield |
The form id | contact |
The field name | name |
Hooks and the Form ID
After some research, I found the Themable Forms module that implements hooks to recursively provide the form id in the build array of form elements. Using this pattern, I implemented hook_form_alter() for providing the form id to my form elements' render arrays, which was the key missing element. I implemented this in my custom module named jcmodule
, in the jcmodule.module
file:
/**
* Implements hook_form_alter().
*/
function jcmodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {
jcmodule_attach_form_id($form, $form_id);
}
/**
* Attaches form id to all form elements.
*
* @param $form
* The form or form element which children should have form id attached.
* @param $form_id
* The form id attached to form elements.
*
* @return array
*/
function jcmodule_attach_form_id(&$form, $form_id) {
foreach (Element::children($form) as $child) {
if (!isset($form[$child]['#form_id'])) {
$form[$child]['#form_id'] = $form_id;
}
jcmodule_attach_form_id($form[$child], $form_id); // recurse for children
}
}
This defined a new value for #form_id
in the $build
array for my form elements. Under the hood it looks something like this
$build = [
"#type" => "textfield",
"#name" => "name",
"#title" => "Your full name",
"#required" => true,
"#form_id" => "contact",
"#weight" => 0,
...
];
To take advantage of the #form_id
values that were added, I implemented a hook in my theme to provide templates for each form element. I used the generic hook, hook_theme_suggestions_alter() so the basic form elements would be covered. This approach provides theme suggestions for input fields, checkbox fields, select fields, etc. Since the hook is generic, wrapping all the logic with the initial check for #form_id
will only be called for form elements, and it leaves room for more logic in the body of the same hook function for other element types in the future.
/**
* Add suggestions by keys
* implements hook_theme_suggestions_alter()
*
* @param array $suggestions
* Existing suggestions
* @param array $variables
* Element variables
* @param string $hook
* Original hook
*/
function jctheme_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
if (
isset($variables['element']['#form_id'])
&& isset($variables['element']['#type'])
&& isset($variables['element']['#name'])
) {
$element = $variables['element'];
$formid = str_replace('-', '_', $element['#form_id']);
$suggestions[] = $hook . '__' . $formid;
$suggestions[] = $hook . '__' . $element['#type'] . '__' . $formid;
$suggestions[] = $hook . '__' . $element['#name'] . '__' . $formid;
$suggestions[] = $hook . '__' . $element['#name'] . '__' . $element['#type'] . '__' . $formid;
}
}
This provided the suggestions I was initially looking for, and more that could be useful in the future
input--name--contact.html.twig # any field named `name` in `contact` form
input--textfield--contact.html.twig # all input text fields in `contact` form
input--contact.html.twig # all input fields in `contact` form
input--name--textfield--contact.html.twig # text field named `name` in `contact` form
To implement, I copied input.html.twig from the stable theme and renamed it to input--name--contact.html.twig
using the new suggestion, so now only this field on this form will be rendered with this template.