0

I have a custom entity type with a "yield" field. I would like to allow users to see the custom entity (full content view-mode) and change "yield" value to regenerate it with calculated values from the yield changement.

I started with a new fieldFormatter for my "yield" (number_decimal) field. The formatter extends DecimalFormatter and builds a little form object instead of displaying a simple markup.

$element = [
  '#type' => 'form',
  '#tree' => TRUE,
];
$element['yield'] = [
  '#type' => 'number',
  '#title' => $this->t('Change yield'),
  '#default_value' => floatval($this->numberFormat($item->value)),
  '#value' => floatval($this->numberFormat($item->value)),
];

$element['submit'] = [
  '#type' => 'submit',
  '#value' => $this->t('Change'),
  '#submit' => ['changeYield'],
];

Form is defined in the full content view, and a click on the submit button send the form. I think this is not the correct way to add a form because #submit callback is never called. I tried different syntax, with static or non-static callback, but it doesn't work.

I would like to find an elegant solution, not hacking and adding JavaScript custom code.

Now the ideal solution is attaching #ajax callback to rebuild the data without sending the page, but I don't know if it possible and if dependencies will be not to strong, because changes done on another fieldFormatter item's values.

3 Answers 3

1

You can't add a form field to an entity display. You have to approach this the other way. Build a form class with the field and the submit button and add the entity content to it. Then you can respond to the posted data in the submit method of the form class and save it to the entity.

For the ideal solution you can attach ajax to the submit button to avoid a page reload.

An alternative option is an entity form using a custom form mode containing one field. This could be set up without code by using blocks for both, the entity content and entity form or only one of them and the other as page. This depends on what the end result should look like. You need a module to control the form modes Form mode control and to put an entity form in a block Entityform block. The entity content can be placed in a block with Views. For a custom entity all of this can be done in code as well.

1

For my uses, I found a solution to embed forms in formatters.

Imagine that an entity type (type A) has a entity-reference field that holds another entity type (type B). Entities of type B holds a third entity type (type C).

Example: A : an entity of type "collection" B : an entity of type "album" C : an entity of type "song".

A collection contains one or more albums, and each album contains some songs.

For the "collection" entity view, I would like to display each album as a little form with a table listing the songs, and allow reorder (draggable) and edit songs data.

In this case, and it work, I created a "album" formatter and a "song_list" form that contains the logic of the draggable/editable table.

The album formatter has a viewElements method like this:

public function viewElements(FieldItemListInterface $items, $langcode) {
    $base_entity = $items->getEntity();

    $elements = [
        '#theme' => 'item_list',
        '#items' => [],
        '#title' => $this->t('Albums listing'),
    ];    
    /** @var \Drupal\my_module\Entity\Album $entity */
    foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {

        $name = $entity->label();
        $name = Xss::filter($name, []);
        $item = [];
        $item['name'] = [
          '#type' => 'markup',
          '#markup' => '<h2>'.$name.'</h2>',
        ];

        $form = \Drupal::formBuilder()->getForm('\Drupal\my_module\Form\AlbumSongListForm', $entity);
        $item['form'] = $form;
        $elements['#items'][] = $item;
    }
    return $elements;
}

I used this formatter as "album" entity formatter in the "collection" view display mode. When I go the the "collection" page, For each album in the collection is displayed a form listing each song. And off course, each form can be submitted to save values. Be careful with multiple forms of the same class on the same page. Indicate explicitly the "form ID" to have unique identifier for each form. This can be done with

$form['#id'] = 'album-'.$album->id().'-song-list-form';

Finally, it's possible to use form's in formatters. I don't know if ajax or other behaviors can be used, but #states works with no problems.

1
  • Yes, this is because you are fetching a form array and setting it into the elements array, where it is rendered later. But I suspect that this is not a good place for it (inside viewElements).
    – Kevin
    Mar 30, 2017 at 17:21
0

Ok. Finally I found a solution based on 4k4 answer.

Instead of attaching ajax or simple submit to the formatter, I created a Form with my needs, and a Block that embed the form.

Form

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;

class YieldChangerForm extends FormBase {

  public function getFormId() {
    return 'my_module_yield_changer';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {

      // Attach some fields here.

      $form['submit'] = [
        '#type' => 'submit',
        '#value' => $this->t('Change'),
      ];
      /*
      $form['submitAjax'] = [
        '#type' => 'button',
        '#value' => $this->t('Change'),
        '#ajax' => [
          'callback' => '::changeYield',
        ],
      ];
      */
    }    

    return $form;
  }

  /*
  public function changeYield(array &$form, FormStateInterface $form_state) {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand(
      '#my-wrapper',
      '<div id="#my-wrapper">Modified content here</div>'
    ));
    return $response;
  }
  */

  /**
   * {@inheritDoc}
   * @see \Drupal\Core\Form\FormInterface::submitForm()
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

    // Handle submit here
}

Block

namespace Drupal\my_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provide a Yield changer block to display
 * on recipes.
 * 
 * @Block(
 *   id = "my_module_yield_changer_block",
 *   admin_label = @Translation("Yield changer"),
 * )
 */
class YieldChangerBlock extends BlockBase {

  /**
   * 
   * {@inheritDoc}
   * @see \Drupal\Core\Block\BlockPluginInterface::build()
   */
  public function build() {
    $form = \Drupal::formBuilder()->getForm('\Drupal\my_module\Form\YieldChangerForm');
    $renderArray = [
      'form' => $form
    ];
    return $renderArray;
  }
}

The block is placed before the entity content and display the Form. From now, I simply reload the page and set a "yield" key with user selected value in the URL.

In the sample, you can see commented code that use ajax callback to send modified data. I don't use it now because I think it's complex to render the data I want to change.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.