Profile picture for user admin
Daniel Sipos
22 May 2018

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('entity', $entity);

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?

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Comments

Akshay 28 Nov 2018 18:19

Getting Error for below line.

$form[$name] = $widget->form($items, $form, $form_state);

Jeffam 24 Jan 2019 18:00

Field Ordering

If you want your fields ordered as configured on the default form mode, you can set the weight from the component like so:

[...]
$form[$name]['#access'] = $items->access('edit');
$form[$name]['#weight'] = $component['weight'];
Chris 08 Mar 2019 04:51

Don't forget the dependency injection for the entityTypeManager

The following code doesn't work in a form unless you use dependency injection for your form and supply the EntityTypeManagerInterface service:

$form_display = $this->entityTypeManager->getStorage('entity_form_display')->load('node.article.custom_form_display');

Here's a link to a tutorial on how to set up the injection: https://www.drupal.org/docs/8/api/services-and-dependency-injection/dependency-injection-for-a-form

Instead of using the current user service in that tutorial, you'll want to change the variable names, and use something like:

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager')
    );
  }

Hope this helps someone!

William 01 Apr 2019 21:00

Thanks! Danny!

Thanks for sharing!
one little question:

$entity = $this->entityTypeManager->getStorage(‘node’)->create([
  'type' => 'article'
]);
$form_state->set('node', $node);

should it be:

$form_state->set('entity', $entity);
Ed 15 Jun 2019 15:42

Default value

Can we have a default value in a field rendered like that?

ChrisNimo 27 Aug 2019 17:49

Strange behaviour

On submit in var $extracted I get an array with field values but the values are the key of the field and not the actual value:
[
status = 'status',
body = 'body',
]
Any Idea what I'm doing wrong here? Thanks for any help!

Chris Nimo 29 Aug 2019 07:29

Sorry

Sorry vor the previous post about empty array extracded. The values are in the entity! Correct reading ftw! Thanks for your great blog articles!

Kind regards from tyrol, austria!
Chris

Jean-Louis Mainguy 16 Sep 2019 17:22

No need to loop through fields

While searching for a way to do this, I came across a snippet that can help you build the form without having to loop through the fields by using the "buildform()" function :

$form_state->set('node', $node);
$form_display = EntityFormDisplay::collectRenderDisplay($node, 'default');
$form_display->buildForm($node, $form, $form_state);
$form_state->set('form_display', $form_display);

On a side-note, has anyone found a way to save every field automatically in a custom form without having to loop / check every field type? Thank you!

Scott 01 Sep 2020 03:04

In reply to by Jean-Louis Mainguy (not verified)

Thanks!

Thanks Jean-Louis, this worked for me while the original code did not (I got "Argument 1 passed to Drupal\Core\Field\WidgetBase::errorElement() must be of the type array, null given" errors). And your example code is simpler as well. Nice job!

Alan Stanley 22 Sep 2020 04:02

Editing?

This technique works beautifully for form creation and the creation of complex content types, but when I try to edit an node of that content type using the same form the widget's javascipt no longer works.

Simple case is I've created a field with an entity reference. If I try to add a second or a third is the "please wait' and the arrow spins for the right amount of time, but the extra element isn't added to the form.

Any ideas?

Alan Stanley 22 Sep 2020 18:12

Ignore previous

The problem I was having with editing turned out to be me asking for a field that didn't exist for a default value. This somehow broke the callback.

Robbie 16 Nov 2020 05:59

You is SMART!

LIFE SAVER. To save the entity in the submit just do

 $form_display = $form_state->get('form_display');
    $entity = $form_state->get('entity');
    $extracted = $form_display->extractFormValues($entity, $form, $form_state);
    $entity->save();
Trent 07 Apr 2021 19:45

Awesomely helpful

This is works very nicely (either looping or using buildForm)! Thank you so much for this article.

Peter 19 Nov 2022 18:11

Doesn't work with my custom widget

I used Console to create a custom entity with bundles to allow me to grab widgets from its form display for my custom form. I simplified your code slightly (removed dependency injection) to this:


$entity = Widgets::create(['type' => 'quote_extra']);
$form_state->set('entity', $entity);
$form_display = \Drupal::service('entity_display.repository')
->getFormDisplay('widgets', 'quote_extra', 'default');
$form_state->set('form_display', $form_display);

I then used your foreach loop to ;pull out the field i am interested in adding to my custom form. This works brilliantly. I see the widget change on my custom form as i change it on the form display for my custom entity. But, it doesnt work for my custom widget.

The widget code is pretty simple:

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['value'] = $element + [
'#type' => 'checkbox',
'#default_value' => !empty($items[0]->value),
'#attributes' => ['class' => ['toSwitch']],
];
$element['#attached']['library'][] = 'sia_mpep/toggle';
return $element;
}

The field does show up but without the JS effect provided by the widget. Is it possible the attached library for the widget is lost somehow?

Peter 19 Nov 2022 19:06

disregard last comment

I found my mistake. My widget was adding its library wrong. Once fixed; this does work correctly. Also, the comment above about requiring dependency injection is not correct. This is not required (but preferred i think for writing unit tests).

Add new comment