Form API
Most custom forms in Drupal extend the FormBase class.
There are three main types of forms:
- A generic form: extend
FormBase - A configuration form that enables administrators to update a module's settings: extend
ConfigFormBase - A form for deleting content or configuration, which provides a confirmation step: extend
ConfirmFormBase
The latter two extend FormBase under the hood, but they have different sets of required methods when extended.
Required Methods When Extending FormBase
getFormId()
Should return a string with a unique ID for your form.
buildForm()
public function buildForm(array $form, FormStateInterface $form_state) {}Add any form elements to the
$formarray. Should return the$formvariable.
validateForm()
public function validateForm(array &$form, FormStateInterface $form_state) {}Access a specific form value using
$form_state->getValue('field_id')or all values using$form_state->getValues().To set a validation error, use the
$form_statevariable like this:
$form_state->setError($form['phone_number'], $this->t('The phone number is too short. Please enter a full phone number.'));
$form_state->setErrorByName('phone_number', $this->t('The phone number is too short. Please enter a full phone number.'));submitForm()
public function submitForm(array &$form, FormStateInterface $form_state) {}You can set a redirect here using
$form_state->setRedirect().
Required Methods When Extending ConfigFormBase
getFormId()buildForm()getEditableConfigNames()
protected function getEditableConfigNames() {}Should return an array of config name strings.
submitForm()
You should save values to the config here.
The
ConfigFormBaseclass has a default implementation, but you can override it if needed.
Required Methods When Extending ConfirmFormBase
getFormId()submitForm()getCancelUrl()
Should return a
Urlobject.
getQuestion()
Should return a string or translatable string.
Form Page
Create the my_module.routing.yml file:
example.form:
path: '/example-form'
defaults:
_title: 'Example form'
_form: '\Drupal\my_module\Form\ExampleForm'
requirements:
_permission: 'access content'Retrieving the Form Programmatically
You may need to render a form inside a custom block, for example:
$form = \Drupal::formBuilder()->getForm('Drupal\example\Form\ExampleForm', $para);
// OR
$form_object = new \Drupal\mymodule\Form\ExampleForm($something_special);
$form_builder->getForm($form_object);Altering a Form
You can alter existing forms or your custom forms using hooks:
hook_form_alter()hook_form_FORM_ID_alter()
Example usage:
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
$form['example']['#title'] = t('Test');
}Form Elements
Form elements are HTML elements represented as render arrays. You can set up form elements inside the buildForm() method of your form class or in a form alter hook.
Sample form element definition:
$form['element'] = [
'#type' => 'textfield',
'#title' => $this->t('Element'),
];Each form element accepts general properties, but different form element types may also have unique properties specific to them.
General Properties:
#title(string) The title of the form element. Should be translated.#description(string) Help or description text for the element. In an ideal user interface, the#titleshould be enough to describe the element, so most elements should not have a description. If you do need one, make sure it is translated. If it is not already wrapped in a safe markup object, it will be filtered for XSS safety.#default_valueThe default value for the element. See also#value.#required(bool) Whether or not input is required for the element.#access(bool) Whether the element is accessible or not. WhenFALSE, the element is not rendered, and the user-submitted value is not considered.#disabled(bool) IfTRUE, the element is shown but does not accept user input.#ajax(array) An array specifying Ajax behavior. See the JavaScript API and AJAX Forms guides for more information.#prefix(string) A prefix to display before the HTML input element. Should be translated. If it is not already wrapped in a safe markup object, it will be filtered for XSS safety.#suffix(string) A suffix to display after the HTML input element. Should be translated. If it is not already wrapped in a safe markup object, it will be filtered for XSS safety.#required_error(string) Override the default error message. "@field_title is required" will be used if this is undefined.#states(array) Information about JavaScript states, such as when to hide or show the element based on input from other elements. Refer toFormHelper::processStates.#title_display(string) Where and how to display the#title. Possible values: before, after, invisible, attribute (for a hover tooltip).#tree(bool)TRUEif the values of this element and its children should be hierarchical in$form_state.FALSEif the values should be flat. See also#parents,#array_parents.#valueUsed to set values that cannot be edited by the user. This should NOT be confused with#default_value, which is for form inputs where users can override the default value. Used by elements like button, hidden, image_button, submit, token, and value.
Common Form Element Types:
textfieldselectcheckbox/checkboxesradiostextareanumbertelemaildateurlrangeentity_autocomplete(accepts unique properties):
$form['reference_nodes'] = [
'#target_type' => 'node',
'#tags' => TRUE,
'#default_value' => $node,
'#selection_handler' => 'default',
'#selection_settings' => [
'target_bundles' => ['article', 'page'],
],
'#autocreate' => [
'bundle' => 'article',
'uid' => <a valid user ID>,
],
];Aside from Form Elements, there are also Render Elements as part of Drupal's Render API. These include types like:
containerfieldsetdetails- and many more.
Conditional Form Fields with #states
Drupal's Form API #states property allows you to easily show, hide, enable, disable, require, or collapse form fields based on values selected or entered in other fields on the form or elsewhere on the page.
States that can be applied to a form field element:
- enabled
- disabled
- required
- optional
- visible
- invisible
- checked
- unchecked
- expanded
- collapsed
The following states may be used when checking values of other fields:
- empty
- filled
- checked
- unchecked
- expanded
- collapsed
- value
Examples:
# 1
'#states' => [
// Show this text field only if the 'other' radio button is selected above.
'visible' => [
// Don't mistake :input for the type of field or a CSS selector --
// it's a jQuery selector.
// You can always use :input or any other jQuery selector here, no matter
// whether your source is a select, radio, or checkbox element.
// In the case of radio buttons, we can select them by their name instead of their ID.
':input[name="colour_select"]' => ['value' => 'other'],
],
],
# 2
'#states' => [
// Show this text field if either the 'other' or 'custom' radio button is selected.
'visible' => [
':input[name="colour_select"]' => [
// User selected 'Other'.
['value' => 'other'],
// Conditional logic. Allowed values are or, xor, and.
'or',
// User selected 'Custom color'.
['value' => 'custom'],
],
],
],
# 3
'#states' => [
'visible' => [
':input[name="field_override_subject"]' => ['checked' => TRUE],
],
],
# 4
$form['example_field']['#states'] = [
'visible' => [
[':input[name="my_select_list"]' => ['value' => 'user']],
],
];
# 5 Node form field
$form['field_some_checkbox']['widget'][0]['value']['#states'] = [
'unchecked' => ['select[name="field_some_select"]' => ['value' => 'a-specific-value']],
];#process and #after_build
$form['#process'][] = 'yourmodulename_form_process';Can be used on a form but is more often added to a form element. It is used to alter how an element works or looks, often breaking complex elements into smaller parts (e.g., turning a #radios element into multiple #radio elements). This is mostly used when you’ve created a custom form element.
$form['#after_build'][] = 'yourmodulename_after_build';Use this when you need to alter the form after all other form alters have been processed.
AJAX
The #ajax property of a form element is an array. Here are the details of its known elements, all of which are optional:
callbackThe callback to handle the server-side Ajax event. More information on callbacks is under "Setting up a callback to process Ajax".
If you use
callback, your function will receive the$formand$form_statefrom the triggering form. You can use$form_stateto access data the user entered into the form.
wrapperThe HTMLidattribute of the area where the content returned by the callback should be placed. Callbacks can return content or JavaScript commands; thewrapperis used for content returns.methodThe jQuery method for placing new content (used withwrapper). Valid options are 'replaceWith' (default), 'append', 'prepend', 'before', 'after', or 'html'. See http://api.jquery.com/category/manipulation/ for more on these methods.effectThe jQuery effect to use when placing the new HTML (used withwrapper). Valid options are 'none' (default), 'slide', or 'fade'.speedThe effect speed to use (witheffectandwrapper). Valid options are 'slow' (default), 'fast', or a specific number of milliseconds.eventThe JavaScript event to respond to. This is selected automatically for the type of form element, but you can override it. For example, the event forsubmit,button, andimage_buttonelements is "mousedown" (not "click").preventA JavaScript event to prevent when the event is triggered. For example, if you use 'mousedown' on a button, you might want to prevent 'click' events from being triggered.disable-refocusDisable automatic refocusing after an Ajax call.progressAn array indicating how to show Ajax processing progress. It can contain:typeType of indicator, either 'throbber' (default), 'bar', or 'none'.messageTranslated message to display.urlFor a bar progress indicator, the URL path for determining progress.intervalFor a bar progress indicator, how often to update it.
urlA\Drupal\Core\Urlto which the Ajax request should be submitted. Defaults to the form or link's URL (or a slightly modified version). It is recommended to omit this key and rely on Drupal’s content negotiation.
Example AJAX callback functions:
# Example 1: Open modal dialog via AJAX.
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand(
$this->t('Need help?'),
"Help text",
['dialogClass' => 'test-popup', 'width' => 500]
));
return $response;
}
# Example 2: Replace form with an updated version, and display messages if any exist.
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
return $form;
}Dependency Injection in Form Class
In PHP 8, dependency injection is simplified. Add these methods to your form class and include the desired services:
/**
* Constructs a new CustomForm object.
*/
public function __construct(
protected ExampleServiceInterface $exampleService,
protected EntityTypeManagerInterface $entityTypeManager
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('example_service'),
$container->get('entity_type.manager')
);
}Note that extending FormBase gives you access to some predefined methods, so there is no need to inject them yourself:
$this->currentUser()$this->messenger()$this->logger('channel')$this->configFactory()
Questions
Which parameters does hook_form_alter() accept?
hook_form_alter(&$form, FormStateInterface $form_state, $form_id);How to add an additional validate function using hook_form_alter()?
$form['#validate'][] = 'function_name'; // Will be called last.How to add an additional validate function using hook_form_alter()
$form['actions']['submit']['#submit'][] = 'function_name'; // Will be called last.
// This can work in some cases:
$form['#submit'][] = 'function_name'; // Will be called last.How to create a render array for an autocomplete of category taxonomy terms?
$form['category'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Category'),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['category'],
],
];How do you automatically hide the "name" field when the "anonymous" checkbox is checked?
// We need to use #states
$form['name']['#states'] = [
'invisible' => [
':input[name="anonymous"]' => ['checked' => TRUE],
],
];