Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tedbow created an issue. See original summary.

tedbow’s picture

Title: Create new revision checkbox for each widget get added to parent form » Create new revision checkbox for each widget gets added to parent form
Issue summary: View changes
tedbow’s picture

Title: Create new revision checkbox for each widget gets added to parent form » Extra node properties each widget gets added to parent form
Issue summary: View changes
FileSize
91.61 KB
tedbow’s picture

Issue summary: View changes
FileSize
68.66 KB

Drupal 7 screenshot

Each nested node has "Published checkbox but no other properties.

tedbow’s picture

Status: Active » Needs review
Issue tags: +Needs tests
FileSize
1.17 KB

Ok here is patch that removes all element in node forms that will show up under "advanced" section.

Should note that this already the behaviour unless the field field is required and the form is created when the parent form is created and not in ajax callback.

tedbow’s picture

Status: Needs review » Needs work

The last submitted patch, 6: ief-extra_node_properties-2667710-5_TEST_ONLY.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review

Setting to "Need Review" because only TEST_ONLY patch failed as expected.

bojanz’s picture

Title: Extra node properties each widget gets added to parent form » Rewrite the base inline form handling
Priority: Normal » Critical
Status: Needs review » Needs work

The problem is much bigger than that.

$controller = $this->entityManager->getFormObject($entity_form['#entity']->getEntityTypeId(), $operation, FALSE);
$controller->setEntity($entity_form['#entity']);

This code gets a form object for the given entity type, then passes it the form.
In the node case, $controller is an instance of Drupal\node\NodeForm.
However, entity forms are not designed to be embedded, they assume they own the form.
Hence, endless conflicts.

We need to rewrite the EntityInlineForm logic completely, and use the form display object directly, just like ContentEntityForm does:

$this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);

Plus the relevant logic in buildEntity / validateForm, etc.

Basically, the InlineForm is an alternative to a ContentEntityForm, and needs to have the same logic (equivalent of D7's field_attach_form).

tedbow’s picture

Status: Needs work » Needs review
FileSize
3.18 KB

Ok, as a start I just replaced:
$controller = $this->entityManager->getFormObject($entity_form['#entity']->getEntityTypeId(), $operation, FALSE);
with

if ($controller instanceof ContentEntityFormInterface) {
      $controller->getFormDisplay($child_form_state)->buildForm($entity_form['#entity'], $entity_form, $child_form_state);
    }
    else {
      $entity_form = $controller->buildForm($entity_form, $child_form_state);
    }

I also removed the test that was checking that advanced properties don't show up.

So now the advanced properties will show up if have the permissions.

tedbow’s picture

Issue summary: View changes
FileSize
136.6 KB

Ok, the current tests pass but all the test are done with "administer nodes" permission so it might not see all the form conflicts

I did check manually for now "advanced" tabs base fields are not duplicated

Should we have test that uses xpath to check that base fields like "authored by" and "authored on" are in the correct part of the form.

Right now it doesn't act like Drupal 7 version because these properties don't show up for child forms.

With #2510274: Add ability to select form Display Mode a sitebuilder could use a different form display to not show authored by, authored on, promoted, and sticky. but this would a pain if you had a number of content types. Also the revision log can't be hidden by form display screen.

I also did a quick test that I could have different author and publication date on a child node. Would this be a good test?

bojanz’s picture

tedbow and myself did some exploration of the problem, here's an update.

When it was time to design IEF for D7, I first looked at subform, which tried to simulate an entire form lifecycle (building, validation, submission), leaving the underlying form oblivious of the embedding. The subform code was long and hard to understand, so instead I decided to imitate field widgets, creating entity forms that are aware of the fact that they are being embedded (via $entity_form['#parents']). Just like field widgets, it meant that their validation and submission methods had to rely on #parents to get the right values from form state. Of course, this meant that the usual form hooks didn't fire, IEF added its own instead.
The assumption was that the IEF context is sufficiently different to warrant different forms (there's simply less screen space), which made the idea of embedding an existing form unattractive anyway.

This is how we arrived at the current D7 situation.
D8 made an accidental mistake, and started calling the original entity forms (ContentEntityForm, represented as $controller) instead. These forms always assume that they own the entire form, not respecting #parents at all, meaning that all kinds of problems are possible (since multiple forms are fighting over the same part of $form_state).

Instead, we go back to the D7 way. The D7 field_attach_* methods have migrated to the EntityFormDisplay object.
That means our code needs to get a form display:

    $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());

and then use the $form_display object to call its buildForm() / validateFormValues() and other methods.

tedbow raised the concern that this would cause us to duplicate all of ContentEntityForm and EntityForm, so I looked into the extra logic those two classes have.
All of ContentEntityForm logic revolves around allowing forms to add form elements for entity fields, instead of using widgets. I am guessing this is due to legacy reasons. That's not something we need to support in IEF at this point, we should be pushing people to use widgets as much as possible. Almost no entity type should need to override the entityForm methods of the InlineForm handler.
The only use case for not using a widget, but doing manual form elements instead is having to cover multiple fields at the same time, but that's an edge case that I'm happy to address in a different issue (one that perhaps has less code than ContentEntityForm).
When it comes to EntityForm, a part revolves around getting the entity from the route (unneeded) and firing various hooks (unneeded). Another part revolves around building $this->entity (as an alternative to us using $entity_form['#entity'] all over the place), but we can do that as cleanup in a different issue too.

tedbow’s picture

Ok here is the first try at it.

I removed all need to get the $controller as in
$controller = $this->entityTypeManager->getFormObject($entity_form['#entity']->getEntityTypeId(), $operation, FALSE);

This removes the need for $child_form_state.

All test pass for me locally. I did have to change 1 line in InlineEntityFormElementWebTest but i wasn't sure when the original was passing before and the newer 1 is more specific.

bojanz’s picture

Status: Needs review » Needs work
   /**
+   * Build from form.
+   *
+   * @param $entity_form
+   * @param $entity
+   * @param $inline_form_state
+   */
+  protected static function buildEntity(&$entity_form, ContentEntityInterface $entity, $inline_form_state) {
+
+    static::copyFormValuesToEntity($entity, $entity_form, $inline_form_state);
+

Invalid docblock, extra newline after opening brace.

+  /**
+   * Build the entity form
+   *
+   * @param array $entity_form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   * @param $operation
+   */
+  protected function buildForm(&$entity_form, FormStateInterface $form_state, $operation) {
+    /** @var ContentEntityInterface $entity */
+    $entity = $entity_form['#entity'];
+    $form_display = static::getFormDisplay($entity, $operation);
+    $form_display->buildForm($entity, $entity_form, $form_state);
+
+    if (!$entity_form['#display_actions']) {
+      unset($entity_form['actions']);
+    }
+  }

There's no reason for this code to live in a separate method, put it back in entityForm().
Also remember that @var typehints need the full namespace.

+    // The entity was already validated in entityFormValidate().
+    $entity->setValidationRequired(FALSE);

Do we still need this?

+  /**
+   * Get form display for entity operation.
+   *
+   * @param ContentEntityInterface $entity
+   * @param string $operation
+   *
+   * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
+   */
+  protected static function getFormDisplay(ContentEntityInterface $entity, $operation) {

Invalid docblock. Verb must be Gets, interface must have namespace, param and return must have descriptions.

+  /**
+   * Copies top-level form values to entity properties
+   *
+   * This should not change existing entity properties that are not being edited
+   * by this form.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface
+   *   The entity the current form should operate upon.
+   * @param array $form
+   *   A nested array of form elements comprising the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected static function copyFormValuesToEntity(ContentEntityInterface $entity, array $form, FormStateInterface $form_state) {
+    // First, extract values from widgets.
+    $extracted = static::getFormDisplay($entity, 'default')->extractFormValues($entity, $form, $form_state);
+
+    // Then extract the values of fields that are not rendered through widgets,
+    // by simply copying from top-level form values. This leaves the fields
+    // that are not being edited within this form untouched.
+    foreach ($form_state->getValues() as $name => $values) {
+      if ($entity->hasField($name) && !isset($extracted[$name])) {
+        $entity->set($name, $values);
+      }
+    }
+  }

We didn't port any other logic that allows covering entity fields not using widgets, so the only piece of this method that works is
$extracted = static::getFormDisplay($entity, 'default')->extractFormValues($entity, $form, $form_state);
We might as well remove the entire method and move that line to the calling method.

tedbow’s picture

// The entity was already validated in entityFormValidate().
+    $entity->setValidationRequired(FALSE);

Do we still need this?

It is still true that entityFormValidate will validate the entity.

It calls validateFormValues which says:

This method invokes entity validation and takes care of flagging them on
* the form.

But I am going to move it here.

if ($entity_form['#save_entity']) {
      // The entity was already validated in entityFormValidate().
      $entity->setValidationRequired(FALSE);
      $entity->save();
    }

Because I think the only reason is to avoid double validation on save and if we aren't saving it seems useless.

tedbow’s picture

Status: Needs work » Needs review
FileSize
13.51 KB
6.16 KB

Changes asked for in #14 excepted 1 noted in #15

  • bojanz committed c2f7a03 on 8.x-1.x authored by tedbow
    Issue #2667710 by tedbow: Rewrite the base inline form handling
    
bojanz’s picture

Status: Needs review » Fixed
Issue tags: -Needs tests

Tweaked the docs and committed. 140 lines of code less, and who knows how many bugs. Thanks, tedbow.

slashrsm’s picture

Great progress. Thank you both! I am sorry I wasn't able to be involved.

Status: Fixed » Closed (fixed)

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