Problem/Motivation

I'm trying to replace the link field on the core menu with an entity browser. But it's proving difficult to get good results. I have it so far as to display the "details" container and the "add entities" button. It opens a modal, displays the view, etc. But once I select entities from the view and it returns to the form it's not refreshing the form. I see the throbber image, but the form doesn't appear to be updated.

Upon looking at the page I can see the the entity id has been added, but it's been added to a generated hidden field instead of the target_id field I had created.

I modeled the render array after a node edit form entity browser widget.

Does anyone know how to get this to work?

Attached is my extremely long field definition. I'm pretty sure it's related to the naming on my divs, or including extra keys that don't need to be there. This is inside of a form_alter.

  $form['content_link'] = [
    '#type' => 'container',
    '#parents' => ['content_link_wrapper'],
    '#weight' => -3,
    'widget' => [
      '#title' => t('Content Link'),
      '#description' => t('The content to be linked in the menu.'),
      '#field_name' => 'content_link',
      '#field_parents' => [],
      '#required' => TRUE,
      '#parents' => ['content_link'],
      '#tree' => TRUE,
      '#id' => 'edit-content-link',
      '#type' => 'details',
      '#open' => TRUE,
      'target_id' => [
        '#type' => 'hidden',
        '#id' => 'edit-content-link-target-id',
        '#attributes' => [
          'id' => 'edit-content-link-target-id',
        ],
        '#default_value' => "",
        '#ajax' => [
          'callback' => [
            'Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget',
            'updateWidgetCallback',
          ],
          'wrapper' => 'edit-content-link',
          'event' => 'entity_browser_value_updated',
        ],
      ],
      'entity_browser' => [
        '#type' => 'entity_browser',
        '#entity_browser' => 'content_hub_page_browser',
        '#cardinality' => 1,
        '#selection_mode' => 'selection_append',
        '#default_value' => [],
        '#entity_browser_validators' => [
          'entity_type' => [
            'type' => 'node',
          ],
        ],
        '#custom_hidden_id' => 'edit-content-link-target-id',
        '#process' => [
          [
            '\Drupal\entity_browser\Element\EntityBrowserElement',
            'processEntityBrowser',
          ],
          [
            'Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget',
            'processEntityBrowser',
          ],
        ]
      ],
      'current' => [
        '#theme_wrappers' => ['container'],
        '#attributes' => [
          'class' => 'entities-list',
        ],
        'items' => [],
      ],
      '#after_build' => [
        [
          'Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget',
          'afterBuild',
        ],
      ],
      '#attached' => [
        'library' => [
          'entity_browser/entity_reference',
        ],
      ],
    ],
  ];

Comments

merauluka created an issue. See original summary.

slashrsm’s picture

Issue tags: +D8Media

Entity browser form element won't do any AJAX reloads for you. Only thing that it does is leaving entity ids of the selected entities in a hidden field. It is up to the form to figure out what to do with that from then on. Hidden element is added automatically and is not called target_id.

Then entity reference field widget that this module provides comes into the equation. It uses EB form element, but it does a bunch of other things too. It creates own hidden element and calls it "target_id". It also adds some JS code that catches the event of adding IDs to the hidden element and triggers AJAX reload based on that. You want to check how this things are done there. Specially: http://cgit.drupalcode.org/entity_browser/tree/src/Plugin/Field/FieldWid..., http://cgit.drupalcode.org/entity_browser/tree/js/entity_browser.entity_... and http://cgit.drupalcode.org/entity_browser/tree/js/entity_browser.common.js

slashrsm’s picture

Status: Active » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

lykyd’s picture

So how do we add Entity Browser widget to custom form ???

DiDebru’s picture

Would interest me too.

woprrr’s picture

@Insasse your can with usage of entity_browser form element available LIKE :

    '#type' => 'entity_browser',
    '#entity_browser' => $entity_browser_id,
    '#cardinality' => $cardinality,

Or more complexe :

      $element['entity_browser'] = [
        '#type' => 'entity_browser',
        '#entity_browser' => $this->getSetting('entity_browser'),
        '#cardinality' => $cardinality,
        '#selection_mode' => $selection_mode,
        '#default_value' => $entities,
        '#entity_browser_validators' => $persistentData['validators'],
        '#widget_context' => $persistentData['widget_context'],
        '#custom_hidden_id' => $hidden_id,
        '#process' => [
          ['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
          [get_called_class(), 'processEntityBrowser'],
        ],
      ];
DiDebru’s picture

Thank you @woprr.

sime’s picture

I think this information is stale, or something changed in the module. It would be great to formalise this technique in a working example. This is what I have so far to add entity browser to the "add link" form on a menu. It doesn't seem to support cardinality properly, and I have not looked at why the value does not "stick" on the form after selection.

/**
 * Implements hook_form_menu_link_content_form_alter().
 */
function MODULE_form_menu_link_content_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  $uri = &$form['link']['widget'][0]['uri'];
  $uri['#type'] = 'entity_browser';
  $uri['#entity_browser'] = 'content_entity_browser';
  $uri['#cardinality'] = 1;
  $uri['#selection_mode'] = EntityBrowserElement::SELECTION_MODE_EDIT;
  $uri['#default_value'] = []; // @todo, assign current default if exists.

}
RAFA3L’s picture

Same problem here, the file don't appear after select it

RAFA3L’s picture

The trick to show the image after select it is adding a custom function like displayCurrentSelection in the entity_browser module. This private function is responsible for displaying the list of files and is not called in any way from a custom widget.

Here is the code with the custom the displayCurrentSelection passing the file id to generate the html markup with the image, basically it should be inside the ajax wrapper id:


    $target_id = $form_state->getValue('content_link');

    if ($target_id['target_id']) {
      $file_id = explode(':', $target_id['target_id'])[1];
    }

    $form['entity_browser'] = [
      '#type' => 'container',
    ];

    $form['entity_browser']['widget'] = [
      '#field_name' => 'content_link',
      '#parents' => ['content_link'],
      '#tree' => TRUE,
      '#id' => 'edit-content-link',
      '#type' => 'container',
      'target_id' => [
        '#type' => 'hidden',
        '#id' => 'edit-content-link-target-id',
        '#attributes' => [
          'id' => 'edit-content-link-target-id',
        ],
        '#ajax' => [
          'callback' => [
            'Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget',
            'updateWidgetCallback',
          ],
          'wrapper' => 'edit-content-link',
          'event' => 'entity_browser_value_updated',
        ],
      ],
      'current' => $this->displayCurrentSelection($file_id),
    ];

    if (!$file_id) {
      $form['entity_browser']['widget']['entity_browser'] = [
        '#type' => 'entity_browser',
        '#entity_browser' => 'test_files_ajax',
        '#cardinality' => 1,
        '#selection_mode' => 'selection_append',
        '#entity_browser_validators' => [
          'entity_type' => [
            'type' => 'file',
          ],
        ],
        '#custom_hidden_id' => 'edit-content-link-target-id',
        '#process' => [
          [
            '\Drupal\entity_browser\Element\EntityBrowserElement',
            'processEntityBrowser',
          ],
          [
            'Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget',
            'processEntityBrowser',
          ],
        ],
      ];

ravi.sidd’s picture

Nobody is able to understand how to integrate entity browser, select an image and then show the image. Please help.

bingelis’s picture

Landed here, in one of the threads issuing the same usage of entity_browser problem in custom forms, and none provides a complete solution/example. Here is what I came to after dealing with the problem.

Appending code in @RAFA3L comment, just basic method in your class of custom form to define representation for selection (i.e. show selected file name, but you could implement something more advanced):

  protected function displayCurrentSelection($file_id) {
    $media = Media::load($file_id);
    $content = $media ?
      $this->t('File: %name', ['%name' => $media->getName()]) :
      $this->t('No files selected. 3');

    return [
      '#type' => 'fieldgroup',
      'content' => [
        '#markup' => $content,
      ],
      // This  value will go to my submitForm handler. 
      'file_id' => [
        '#type' => 'value',
        '#value' => $file_id,
      ],
    ];
  }

FIle name got shown up, but only once, not updated when wanted to replace ir with different file.
This behavior relates to incompatible properties of 'entity_browser' element:

'#cardinality' => 1,
'#selection_mode' => 'selection_append',

Staying with '#cardinality' => 1, one should used '#selection_mode' => 'selection_edit', but for me it resulted in php error "Used entity browser selection display does not support preselection". If you manage, use selection display which supports preselection, but here's my workaround. Firstly, edit ajax callback of 'target_id'

'callback' => [get_class($this), 'updateWidgetCallback'],

And define own callback method in custom form class:

  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -1);
    $parent = NestedArray::getValue($form, $parents);
    // Clear 'target_id' value, to be able append it with new selection,
    // when entity_browser configured with '#cardinality' => 1.
    $parent['target_id']['#value'] = '';
    return $parent;
  }
bingelis’s picture

Landed here, in one of the threads issuing the same usage of entity_browser problem in custom forms, and none provides a complete solution/example. Here is what I came to after dealing with the problem.

Appending code in @RAFA3L comment, just basic method in your class of custom form to define representation for selection (i.e. show selected file name, but you could implement something more advanced):

  protected function displayCurrentSelection($file_id) {
    $media = Media::load($file_id);
    $content = $media ?
      $this->t('File: %name', ['%name' => $media->getName()]) :
      $this->t('No files selected. 3');

    return [
      '#type' => 'fieldgroup',
      'content' => [
        '#markup' => $content,
      ],
      // This  value will go to my submitForm handler. 
      'file_id' => [
        '#type' => 'value',
        '#value' => $file_id,
      ],
    ];
  }

FIle name got shown up, but only once, not updated when wanted to replace ir with different file.
This behavior relates to incompatible properties of 'entity_browser' element:

'#cardinality' => 1,
'#selection_mode' => 'selection_append',

Staying with '#cardinality' => 1, one should used '#selection_mode' => 'selection_edit', but for me it resulted in php error "Used entity browser selection display does not support preselection". If you manage, use selection display which supports preselection, but here's my workaround. Firstly, edit ajax callback of 'target_id'

'callback' => [get_class($this), 'updateWidgetCallback'],

And define own callback method in custom form class:

  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -1);
    $parent = NestedArray::getValue($form, $parents);
    // Clear 'target_id' value, to be able append it with new selection,
    // when entity_browser configured with '#cardinality' => 1.
    $parent['target_id']['#value'] = '';
    return $parent;
  }
bingelis’s picture

Landed here, in one of the threads issuing the same usage of entity_browser problem in custom forms, and none provides a complete solution/example. Here is what I came to after dealing with the problem.

Appending code in @RAFA3L comment, just basic method in your class of custom form to define representation for selection (i.e. show selected file name, but you could implement something more advanced):

  protected function displayCurrentSelection($file_id) {
    $media = Media::load($file_id);
    $content = $media ?
      $this->t('File: %name', ['%name' => $media->getName()]) :
      $this->t('No files selected. 3');

    return [
      '#type' => 'fieldgroup',
      'content' => [
        '#markup' => $content,
      ],
      // This  value will go to my submitForm handler. 
      'file_id' => [
        '#type' => 'value',
        '#value' => $file_id,
      ],
    ];
  }

FIle name got shown up, but only once, not updated when wanted to replace ir with different file.
This behavior relates to incompatible properties of 'entity_browser' element:

'#cardinality' => 1,
'#selection_mode' => 'selection_append',

Staying with '#cardinality' => 1, one should used '#selection_mode' => 'selection_edit', but for me it resulted in php error "Used entity browser selection display does not support preselection". If you manage, use selection display which supports preselection, but here's my workaround. Firstly, edit ajax callback of 'target_id'

'callback' => [get_class($this), 'updateWidgetCallback'],

And define own callback method in custom form class:

  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -1);
    $parent = NestedArray::getValue($form, $parents);
    // Clear 'target_id' value, to be able append it with new selection,
    // when entity_browser configured with '#cardinality' => 1.
    $parent['target_id']['#value'] = '';
    return $parent;
  }
bingelis’s picture

amadady@gmail.com, you should implement representation for selection.
Just basic example to show selected filename:

  protected function displayCurrentSelection($file_id) {
    $media = Media::load($file_id);
    $content = $media ?
      $this->t('File: %name', ['%name' => $media->getName()]) :
      $this->t('No files selected.');

    return [
      '#type' => 'fieldgroup',
      'content' => [
        '#markup' => $content,
      ],
    ];
  }

Similar approach would be to show media thumbnail according your needs.

oknate’s picture

Title: Add Entity Browser widget to custom form » Simplify adding Entity Browser form element to custom form
Version: 8.x-1.0-alpha9 » 8.x-2.x-dev
Component: Field widget » Miscellaneous
Category: Support request » Feature request
Status: Closed (fixed) » Needs work

Re-opening this. I have also tried to do this in the past and it was painful.

Ideally there'd be a way to include the current selection in the stand-alone element, thereby providing ajax buttons as well as a display of the currently selected items.

PrineShazar’s picture

+1

This is very much needed. Lots of scenarios where users may not want to navigate to a node edit form, and so may want to edit existing nodes inline from a convenient place.

Dave Reid’s picture

Agreed, being able to easily and independently use the entity browser FAPI element seems like we need a lot of wrapping boilerplate that would be either good to have an example documented or provide some of the boilerplate by default.

Thomas.Schnitzler’s picture

I got a mix of RAFA3Ls and Bigelis' solutions up and running, but this has a major drawback: The default functions for the preview items do not work. E.g.:

  • Sorting
  • Removing, replacing and editing of items

Any hope, that this will get any boilerplate code or detailed documentation?

Dave Reid’s picture

I ended up abstracting some of the code from Entity Browser Block into a Helper module EntityBrowserFormTrait so I could re-use it for form and plugins-with-forms classes. Feel free to file issues with requests An example usage in a block plugin form:

use Drupal\helper\EntityBrowserFormTrait;

class MyBlock extends BlockBase {

  use EntityBrowserFormTrait;

  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);

    // Entity Browser element for background image.
    $form['background_image'] = $this->getEntityBrowserForm(
      'my_entity_browser_id', // Entity Browser config entity ID
      $this->configuration['background_image'], // Default value as a string
      1, // Cardinality
      'preview' // The view mode to use when displaying the entity in the selected entities table
    );
    // Convert the wrapping container to a details element.
    $form['background_image']['#type'] = 'details';
    $form['background_image']['#title'] = $this->t('Background image');
    $form['background_image']['#open'] = TRUE;

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    parent::blockSubmit($form, $form_state);
    $this->configuration['background_image'] = $this->getEntityBrowserValue($form_state, 'background_image');
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    /** @var \Drupal\media\MediaInterface $media */
    if (!empty($this->configuration['background_image']) && $media = static::loadEntityBrowserEntity($this->configuration['background_image'])) {
       // Do something with the loaded entity.
    }
  }

}
paul_leclerc’s picture

Thanks #21 :)
I use your Trait, but I had to replace

$entity_ids = $form_state->getValue(array_merge($parents, ['browser', 'entity_ids']), '');

by

$entity_ids = $form_state->getValue('browser')['entity_ids'];

I still need to understand why it didnt work and make some more dynamic or using shared var.
And we had some code to allow multi fields.

We could push a MR to your Trait if we want.

PrineShazar’s picture

Thanks Dave!

Yasser Samman’s picture

#21 + #22 saved my life..

lapaty’s picture

Thanks Dave #21 Saved my life too!! ;)

Joao Sausen’s picture

#21 fixed it too, but a better fix is to add "#tree" => TRUE, to the $element in the getEntityBrowserForm method.

arnaud-brugnon’s picture

#21 works for me in custom form only if we had #tree = TRUE

  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['background_image'] = $this->getEntityBrowserForm(
      'paragraphs_library_items', // Entity Browser config entity ID
      $this->configuration['background_image'], // Default value as a string
      1, // Cardinality
      'preview' // The view mode to use when displaying the entity in the selected entities table
    );
    
    // Convert the wrapping container to a details element.
    $form['background_image']['#type'] = 'details';
    $form['background_image']['#title'] = $this->t('Background image');
    $form['background_image']['#open'] = TRUE;
    $form['background_image']['#tree'] = TRUE;
    return $form;
  }
florianmuellerCH’s picture

Thank you all for your work! #21 and the info in #22 helped me a lot!

I added a JS behavior to make the selected elements sortable. To do that I had to add my library and a class in the base element:

    $element = [
      '#type' => 'details', // I added this directly here
      '#open' => $default_open, // I added this as a parameter
      '#title' => $title, // I added this as a parameter
      '#attributes' => [
        'id' => Html::getUniqueId('entity-browser-' . $entity_browser_id . '-wrapper'),
        'class' => [
          'entity-browser-form-trait-element', // NEW CLASS for JS behavior
        ]
      ],
      '#attached' => [
        'library' => [
          'my_module/entity-browser-trait' // ATTACH new library
        ]
      ]
    ];

Be sure to add the dependency to core/sortable in your libraries.yml.

(function ($) {

  Drupal.behaviors.entityBrowserTraitSortable = {
    attach: function (context, settings) {
      $(once('sortable-entity-browser-images', '.entity-browser-form-trait-element table.responsive-enabled tbody')).each(function() {
        let $tbody = $(this);
        let $parent = $tbody.closest('.entity-browser-form-trait-element');
        $tbody.find('>tr').css('cursor', 'move'); // This is a convenience thing and could be moved to CSS
        Sortable.create($tbody[0], {
          onEnd: function (e) {
            let $to = $(e.to);
            let list = [];
            $to.children('tr').each(function() {
              list.push($(this).attr('data-entity-id'));
            });
            $parent.find('input[type="hidden"].eb-target').val(list.join(' '));
          }
        });

      })
    },
  };

})(jQuery);

gnuget’s picture

#21 worked for me too.

Thanks!!!

tlo405’s picture

I've been using the code in #21 for a while on my site and it has worked great! I had a custom ckeditor plugin that opened up a modal that contained a few form fields, one of which needed to open our custom entity browser. However, I've been attempting to upgrade to ckeditor5, and I can not get this functionality to work any longer.

Clicking the button on the ckeditor5 toolbar opens up my modal successfully, and my other fields work...but I can't get the custom entity browser field to work correctly. Once I make a selection, I still see a 'No items selected yet' message. I would expect the image that I selected to appear there. Even if I hardcode media:123 for example as the default value, I still see the 'no items selected yet' message.

Just wondering if anyone has seen a similar issue? I really don't know what could be causing this to happen.