TextareaWidget::errorElement() does not consult violation property path.

Created on 23 August 2021, almost 4 years ago
Updated 18 January 2023, over 2 years ago

Problem/Motivation

When attempting to add a validation constraint on a text_long field (without summary), I ran into an issue where the violation path (string) $delta . '.value' did not actually attach the error message to the value property, but rather to the whole widget.

Looking at Drupal\text\Plugin\Field\FieldWidget\TextareaWithSummaryWidget, we see ::errorElement() like so:

  /**
   * {@inheritdoc}
   */
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
    $element = parent::errorElement($element, $violation, $form, $form_state);
    return ($element === FALSE) ? FALSE : $element[$violation->arrayPropertyPath[0]];
  }

which is fine and good. Provided a $violation->arrayPropertyPath of 'value', a violation targeting field_wysiwyg.value will attach appropriately.

TextareaWidget is somewhat different:

  /**
   * {@inheritdoc}
   */
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
    if ($violation->arrayPropertyPath == ['format'] && isset($element['format']['#access']) && !$element['format']['#access']) {
      // Ignore validation errors for formats if formats may not be changed,
      // such as when existing formats become invalid.
      // See \Drupal\filter\Element\TextFormat::processFormat().
      return FALSE;
    }
    return $element;
  }

If called directly, it won't consult the arrayPropertyPath and thus will target both the value and the format, leading to unfortunate issues like this.

Steps to reproduce

Create a constraint and validator targeting a text_long field using a textarea widget, and attempt to target the `value` property of the field in the validator like so:

        $this->context->buildViolation($constraint->someMessage)
          ->atPath((string) $delta . '.value')
          ->setInvalidValue($text)
          ->addViolation();

Notice that both the format and the value fields are highlighted, that an error message appears under each, and that the link to the field is constructed inaccurately (e.g. it will link to /node/11463/edit#edit-field-content-block-0-subform-field-wysiwyg-0 when it should link to /node/11463/edit#edit-field-content-block-0-subform-field-wysiwyg-0-value.

Proposed resolution

My proposed fix is to consult arrayPropertyPath in TextareaWidget and then remove the override in TextareaWithSummaryWidget.

Like so:

TextareaWidget:


  /**
   * {@inheritdoc}
   */
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
    if ($violation->arrayPropertyPath == ['format'] && isset($element['format']['#access']) && !$element['format']['#access']) {
      // Ignore validation errors for formats if formats may not be changed,
      // such as when existing formats become invalid.
      // See \Drupal\filter\Element\TextFormat::processFormat().
      return FALSE;
    }
    if (($property = $violation->arrayPropertyPath[0]) && !empty($element[$property])) {
      return $element[$property];
    }
    return $element;
  }

Remaining tasks

I'll upload a patch here as soon as I remember how.

πŸ› Bug report
Status

Active

Version

10.1 ✨

Component
TextΒ  β†’

Last updated 11 days ago

Created by

πŸ‡ΊπŸ‡ΈUnited States natedouglas

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¦πŸ‡ΊAustralia gordon Melbourne

    I have just ran across this issue and I do not think this is a good solution. It fixes the TextAreaWidget and descendants but not any other type of field will be similar (Single widget which has multiple fields).

    From the example above.

    $this->context->buildViolation($constraint->someMessage)
              ->atPath((string) $delta . '.value')
              ->setInvalidValue($text)
              ->addViolation();
    

    When creating the violation as above using the `::AtPath('value')`, the WidgetBase::flagErrors() is then splitting all the violations into 2 groups. A delta list and an item list. The delta list is basically, the violation has the path of `body.0.value` and everything else goes into the item list.

    Since this is a delta list it uses the delta and anything after the delta is discarded (In this case the `value` is not used). In reality it should use the rest of the path after the delta to find the element that the error should be added to.

  • πŸ‡ΊπŸ‡ΈUnited States bapplejax

    This re-roll is to make the patch from #9 apply cleanly to 10.3.x. It involved adding some more use statements and adjusting line numbers from the previous patch.

  • πŸ‡¬πŸ‡·Greece idimopoulos

    idimopoulos β†’ made their first commit to this issue’s fork.

  • πŸ‡¬πŸ‡·Greece idimopoulos

    You are correct @gordon. It seems to be a more generic issue of the form errors method. I am going to work on it for a bit and try to write a test on this. Setting this to needs review only for an initial test run.

  • The Needs Review Queue Bot β†’ tested this issue.

    While you are making the above changes, we recommend that you convert this patch to a merge request β†’ . Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)

  • Pipeline finished with Failed
    25 days ago
    Total: 671s
    #533068
  • πŸ‡¬πŸ‡·Greece idimopoulos

    Hiding the patch files and setting this to be a bug in the field system instead.

  • Pipeline finished with Success
    24 days ago
    Total: 1690s
    #533862
  • πŸ‡¬πŸ‡·Greece idimopoulos

    Mainly, from what I could see, many widgets that represents element with a single property (like the mainProperty is just a single one like value or target_id), they often tend to return that property as part of the \Drupal\Core\Field\WidgetBase::errorElement. This is why it does work for some fields. For others, that simply return the element, more specific targeting is required.
    I have created a patch that tries to descend further into the element in order to try and pinpoint the exact property to add the violation to, according to the property path. It also further checks whether the error element is already returning the appropriate target.

    Note, that for this to work properly, a violation constraint needs to be added properly as well, i.e. using the ::addPropertyConstraints instead of the generic addConstraint.

    The initial MR seemed to have broken multiple tests which revealed the second point about the ::errorElement already returning the correct property from widgets.

    I am still not sure where to write tests for this though :/ any ideas? because it would be easier to write it as a FunctionalTest using the inline_form_error which adds links that can be clicked (that reveals the problem) but seems too much for a widget base. But a PhpUnit test would require quite some mocking (?).

    I am adding the label needs tests.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    May need a test module that does what the steps mention, or if an existing module comes close and can be extended.

  • πŸ‡¬πŸ‡·Greece idimopoulos

    I will try to create a test but I cannot do it this week (sorry).
    What I have found is that there is a generic error with the inline_form_errors and its attempt to create links to the fields.

    The problem is when the constraint is added to the element rather than a property of the element e.g. (example from our code)

    function ..._entity_bundle_field_info_alter(array &$fields, EntityTypeInterface $entity_type, string $bundle): void {
    .
    .  
        /** @var \Drupal\field\Entity\FieldConfig[] $fields */
          $fields['short_id']->addPropertyConstraints('value', [
            'Regex' => ['pattern' => '/^[a-z0-9-]{4,26}$/'],
          ]);
          $fields['short_id']->addConstraint('UniqueShortIdInsensitive');
        }
    

    In the above, we are adding one property constraint, and one constraint on the whole element.
    You may consider a case where you want all values of a multi cardinality field to be different even though above, we are trying to keep a unique value in the database.
    Disclaimer: You may claim that the addConstraint method should be instead addPropertyConstraint. You are correct, it might be wrong design. However, it does not solve the underlying issue, as a constraint that is needed to be set on the element level is easily to imagine as I mentioned above.

    So, there are 2 issues with the inline_form_errors. The first one was fixed (for us, though it needs tests and review) with my patch above. There was a problem that WidgetBase::flagErrors did not descend more than the delta, thus the ID constructed for the inline_form_errors, was edit-my-field-0 instead of edit-my-field-0-value. By default, only the input child is receiving an ID, thus the link from inline_form_errors which would point to href="#edit-my-field-0" would not lead anywhere. As mentioned, now it properly descends.

    However, the main problem here, is with the element level constraints. By default, form-element.html.twig does not add an ID on the element level. So, the element level link from inline_form_errors will not point anywhere with the href="#edit-my-field".
    The problem further expands because, while I did workaround it a bit on my project by trying to force an ID to the elements, there are some cases where this would create conflicts. For example, a select filter and a field with the same name, would have to produce the same ID, so I cannot force it for all form elements in any case without considering what else is in the page. So I have built some exceptions in our code.
    But even further, \Drupal\inline_form_errors\FormErrorHandler::displayErrorMessages seems to detect the wrong ID anyway, so if I set it to the element level, it will still add the fragment edit-my-field even if I add a custom ID, so the above cannot be fixed with the \Drupal\Component\Utility\Html::getUniqueId as it might make it edit-my-field--2 but the fragment will not change accordingly.
    The complexity (not complaining about the complexity) of the element -> widget -> delta -> property of the form API does not seem to be respected from the inline_form_errors.

    An easy way to reproduce this (I have to write the test with the inline_form_errors module and a custom constraint) is to simply create a constraint, add it to an element with the field_info_alter hook, fail the constraint, check the link from inline_form_errors, and confirm that the fragment does not match the ID.

  • πŸ‡§πŸ‡ͺBelgium herved

    MR !12521 seems to solve things partially.

    Maybe the inline_form_error deserves its own issue, not sure...
    But many errors are attached to the field itself (not properties) and in such cases inline_form_error links points to anchors that do not exist in the form. This happens on single element fields.

    To me it's very strange and suspicious why field-multiple-value-form.html.twig does not print the outer div when not multiple, along with attributes and #id of the element. So something like
    {% if multiple %}
    ...
    {% else %}

    {% for element in elements %}
    {{ element }}
    {% endfor %}

    {% endif %}

    This may be a decent way to solve that issue.

Production build 0.71.5 2024