How to render entity field widgets inside a custom form
In an older article we looked at how to render an entity form programatically using custom form display modes. Fair enough. But did you ever need to combine this form with a few form elements of yours which do not have to be stored with the corresponding entity? In other words, you need to be working in a custom form…
What I used to do in this case was write my own clean form elements in a custom form and on submit, deal with saving them to the entity. This is not a big deal if I am dealing with simple form elements, and of course, only a few of them. If my form is big or complex, using multivalue fields, image uploads and stuff like this, it becomes quite a hassle. And who needs that?
Instead, in our custom form, we can load up and add the field widgets of our entity form. And I know what you are thinking: can we just build an entity form using the EntityFormBuilder
service, as we saw in the previous article, and just copy over the form element definitions? Nope. That won’t work. Instead, we need to mimic what it does. So how do we do that?
We start by creating a nice form display in the UI where we can configure all our widgets (the ones we want to show and in the way we want them to show up). If the default form display is good enough, we don’t even need to create this. Then, inside the buildForm()
method of our custom form we need to do a few things.
We create an empty entity of the type that concerns us (for example Node) and store that on the form state (for the submission handling that happens later):
$entity = $this->entityTypeManager->getStorage(‘node’)->create([
'type' => ‘article’
]);
$form_state->set(‘node’, $node);
Next, we load our newly created form display and store that also on the form state:
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = $this->entityTypeManager->getStorage('entity_form_display')->load('node.article.custom_form_display');
$form_state->set('form_display', $form_display);
You’ll notice that the form display is actually a configuration entity whose ID is made up of the concatenation of the entity type and bundle it’s used on, and its unique machine name.
Then, we loop over all the components of this form display (essentially the field widgets that we configure in the UI or inside the base field definitions) and build their widgets onto the form:
foreach ($form_display->getComponents() as $name => $component) {
$widget = $form_display->getRenderer($name);
if (!$widget) {
continue;
}
$items = $entity->get($name);
$items->filterEmptyItems();
$form[$name] = $widget->form($items, $form, $form_state);
$form[$name]['#access'] = $items->access('edit');
}
This happens by loading the renderer for each widget type and asking it for its respective form elements. And in order for it to do this, it needs an instance of the FieldItemListInterface
for that field (which at this stage is empty) in order to set any default values. This we just get from our entity.
And we also check the access on that field to make sure the current user can access it.
Finally, we need to also specify a #parents
key on our form definition because that is something the widgets themselves expect. It can stay empty:
$form['#parents'] = [];
Now we can load our form in the browser and all the configured field widgets should show up nicely. And we can add our own complementary elements as we need. Let’s turn to the submit handler to see how we can easily extract the submitted values and populate the entity. It’s actually very simple:
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = $form_state->get('form_display');
$entity = $form_state->get('entity');
$extracted = $form_display->extractFormValues($entity, $form, $form_state);
First, we get our hands on the same form display config and the entity object we passed on from the form definition. Then we use the former to “extract” the values that actually belong to the latter, from the form state, into the entity. The $extracted
variable simply contains an array of field names which have been submitted and whose values have been added to the entity.
That’s it. We can continue processing our other values and save the entity: basically whatever we want. But we benefited from using the complex field widgets defined on the form display, in our custom form.
Ain't that grand?