Add new hooks

hook_form_paragraphs_subform_alter()
hook_form_paragraphs_subform_TYPE_alter()
hook_form_paragraphs_subform_WIDGET_alter()
hook_form_paragraphs_subform_WIDGET_TYPE_alter()

When trying to form alter paragraphs for an individual type of paragraph is extremely difficult having to pick through the parent forms to and to edit a single type of form has a lot of overheard.

With these 2 alter hooks you can quickly alter the subform for and not take up too much processing time.

Members fund testing for the Drupal project. Drupal Association Learn more

Comments

gordon created an issue. See original summary.

gordon’s picture

Primsi’s picture

Status: Needs review » Needs work

Thanks for the patch! Discussed this with @Berdir a bit. This could be useful.

+++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
@@ -652,6 +652,8 @@ class InlineParagraphsWidget extends WidgetBase {
+        \Drupal::ModuleHandler()->alter([ 'paragraphs_component_form', 'paragraphs_component_' . $paragraphs_entity->get('type')[0]->target_id . '_form' ], $element['subform'], $form_state, $delta);

Is this the right syntax for ModuleHandler::alter? From documentation I see ::alter($type, &$data, &$context1 = NULL, &$context2 = NULL)

I think we are also missing:

  1. documentation in api.php
  2. tests (we have a paragraphs_tests.module where we can implement this hook for testing)

@Berdir also suggested to have different names for experimental and classic, so that checking for that is not needed.

seanB’s picture

+1 for this!

You can add multiple types for alters:

@param string|array $type
A string describing the type of the alterable $data. 'form', 'links', 'node_content', and so on are several examples. Alternatively can be an array, in which case hook_TYPE_alter() is invoked for each value in the array, ordered first by module, and then for each module, in the order of values in $type. For example, when Form API is using $this->alter() to execute both hook_form_alter() and hook_form_FORM_ID_alter() implementations, it passes array('form', 'form_' . $form_id) for $type.

I added small changes to the patch and added documentation. Didn't have time for tests yet (since there are a lot of hooks we want to check).

  • Changed the hook from hook_paragraphs_component_form_alter() to hook_form_paragraphs_subform_alter() since a form alter is probably what people will be looking for.
  • Added specific hooks for the classic/experimental widgets, but kept the generic ones in case you want to do something for both at the same time.

One thing I noticed is you do'nt have easy access to the paragraphs entity. Not sure if you need it, but this could be something to consider adding.

Any thoughts?

gordon’s picture

Thanks for the documentation.

I agree with you about adding the paragraph entity to the but problem being that we can't pass anymore parameters. However I do think that you might be able to get the entity from $element

seanB’s picture

We could add an example on how to get the paragraph from $element in the documentation to help people?

Primsi’s picture

Yes, that would be a good idea. I think we are doing it in the widget itself multiple times, so it can be just copied from there.

gordon’s picture

Status: Needs work » Needs review
FileSize
7.38 KB
2.59 KB

I was investigating some things with views and found that they use $form_state->set() to save the current view to the form storage for anyone to get the current view. I thought that if it is good enough for views it is good enough for paragraphs.

So now using the following in the alter().

$paragraph = $form_state->get('paragraph')

will return you the current paragraph that is being altered.

I didn't unset the paragraph at the end since there is no $form_state->del('paragraph') and the only way to do it is to get the entire storage and unset the paragraph and set it again. Not a very elegant method and in the end it is only going to hold the pointer to the last one outside the hooks and I don't think this is going to cause any memory issues.

RajabNatshah’s picture

+1
Using this patch:

hook_form_paragraphs_subform_alter()
hook_form_paragraphs_subform_TYPE_alter()
hook_form_paragraphs_subform_WIDGET_alter()
hook_form_paragraphs_subform_WIDGET_TYPE_alter()
RajabNatshah’s picture

Issue summary: View changes
yongt9412’s picture

Status: Needs review » Needs work

Looks great, we need tests now. :)

acrosman’s picture

Issue tags: +Needs tests
sgurlt’s picture

Issue summary: View changes
FileSize
142.97 KB

Hm just to clarify, this patch will not allow me to add additional fields to the Paragraphs Form Widget itself, like for example adding another value to the "Add Mode" array.

It just allows me to change the form fields within a paragraph right?

miro_dietiker’s picture

Yeah correct. We would need this in addition. We could also pluginify the add mode... There is a lot code distributed inside Paragraphs around the add modes. I'd be happy to see this removed from the widget spaghetti and put into more specific code / plugins. But the problem is that we did not yet understand how deeply other/new add modes need to be entangled into the internal structure.

Jacine’s picture

Great patch! Thank you :)

yasmeensalah’s picture

I re-rolled the last patch to the current 8.x-1.x.

Mohammed J. Razem’s picture

Status: Needs work » Needs review

The last submitted patch, 4: 2868155-4.patch, failed testing. View results

swilmes’s picture

Am I doing something wrong? I've added the patch and checked that its applied, but when I call mymodule_form_paragraphs_subform_alter(array &$subform, FormStateInterface &$form_state, $delta) {} and turn on my debugger, it never triggers when visiting the node add page that contains a paragraph.

letrotteur’s picture

After applying the patch, I can use hook_form_paragraphs_subform_alter() to restrict access to a field like this

$subform['field_name']['#access'] = FALSE;

but can't seem to get the visibility through ['#states'] using something like this

$subform['field_name']['#states'] = [
    'visible' => [
      ':input[name="field_another_field"]' => ['value' => 'some_value']
    ]
  ];

Am I doing something wrong?

StryKaizer’s picture

Works fine here. Thx!

@letrotteur If you want states for other fields from the same paragraph entity, use following structure, works here.

$subform['field_name']['#states'] = ['visible' => [[':input[name="field_paragraphs[' . $delta . '][subform][field_another_field]"]' => [["value" => "some_value"]],]],];

Or if you dont want to hardcode the paragraphs field you can build the parent trail like this

$parents = $subform['#parents'];
$parents_input_name = array_shift($parents);
$parents_input_name .= '[' . implode('][', $parents) . ']';

$subform['field_name']['#states'] = ['visible' => [[':input[name="' . $parents_input_name . '[field_another_field]"]' => [["value" => "some_value"]],]],];

miro_dietiker’s picture

Great help, plz help to document how to handle states either in documentation pages, some txt or example code in example module or paragraphs demo.

With various nesting levels like containers, it's important to create the states key dynamically.

How about creating an issue to offer a helper to simplify handling states if not yet existing?

and_daz’s picture

Applied patch 2868155-16.patch.
I have a dropdown where I want to conditionally hide and show a image field or a text field based on what was selected from the dropdown.
The following code accomplishes that beautifully (see attached images for example):

function custom_module_form_paragraphs_subform_alter(array &$subform, FormStateInterface &$form_state, $delta) {
    $paragraph = $form_state->get('paragraph');
    $paragraph_type = $paragraph->getType();
    if($paragraph_type == 'media_with_text') {
        $subform['field_media_image']['#states'] = array(
            'visible' => array(
                'select[name="field_content_segments[' . $delta . '][subform][field_media_type_upload]"]' => array('value' => 'image')
            )
        );
        $subform['field_media_youtube_video']['#states'] = array(
            'visible' => array(
                'select[name="field_content_segments[' . $delta . '][subform][field_media_type_upload]"]' => array('value' => 'youtube')
            )
        );
    }
}

I am wondering is there a way to also make these the fields required depending on what was selected from the drop-down? Say I select "image" from the drop-down I want the image field to be required.

kosher’s picture

I would love to see this for Drupal 7.

iampuma’s picture

Just the hooks what I was looking for, thanks!

Anybody’s picture

This looks quite good. Do we have enought tests for RTBC yet?

miro_dietiker’s picture

Status: Needs review » Needs work

@Anybody there is zero test coverage at all here. The test module should implement these hooks and a test needs to represent a scenario that makes sense - such as adding an action.

and_daz’s picture

I was able to solve my required problem #23. If anyone is interested I would be happy to share the code.

pavlosdan’s picture

Just tried these hooks to add validation to the subform using:

$subform['#element_validate'][] = 'my_validation';

In the alter hook the $form_state->get('paragraph')->getParagraphType()->id(); returns the expected paragraph type.

In the validation function though it returns the type of whatever the last paragraph is in the widget.

I suspect this would have something to do with what is mentioned in #8:

"I didn't unset the paragraph at the end since there is no $form_state->del('paragraph') and the only way to do it is to get the entire storage and unset the paragraph and set it again. Not a very elegant method and in the end it is only going to hold the pointer to the last one outside the hooks and I don't think this is going to cause any memory issues."

Would this hooks be the wrong hooks to use to add a validation to a specific paragraph type form?

Berdir’s picture

Yes, the correct approach to add validation to a specific type is to use a validation constraint, implement hook_entity_type_alter() and add it there. See https://www.drupal.org/node/2438011, a quick google search also pointed me to https://www.sitepoint.com/drupal-8-entity-validation-and-typed-data-demo..., https://www.drupalwatchdog.com/volume-5/issue-2/introducing-drupal-8s-en..., did not review them but they look pretty good.

I see no mention of hook_field_widget_form_alter()/hook_field_widget_WIDGET_TYPE_form_alter() in this issue, which confuses me a bit.

I don't see much added benefit of this over those generic hooks. They have $context which contains the $delta, $items and $widget. It's *slightly* more complicated to check which widget type it is ($widget instanceof ParagraphsWidget) and the one thing that's pretty tricky right now is the paragraph type. But I think we can make that available simply by adding something like $elements['#paragraph_type'] in the widgets and others things if we deem them necessary (or we can add methods to the widget to get that information.

That also has the advantage that it's one level higher and doesn't just contain the subform but also behaviors, actions and anything else you might want to mess with.

If I'm missing something then please explain that, otherwise my suggestion would be to focus on making the necessary information available to that existing hook.

What's missing and where I could see a custom hook being useful is to have not just a single item form but the whole widget, including the add form elements as we for example have use cases to limit allowed nesting and I built that using some pretty tricky recursive function.

miro_dietiker’s picture

Connecting an issue about a use case where we proposed to test the hooks first.

sgurlt’s picture

Assigned: gordon » sgurlt

Hey,

we are going to work on the required tests starting from tomorrow. As soon as we got anything showable we will let you know.

Berdir’s picture

the limitation of not being able to alter the whole widget was also noticed in core: #2940201: hook_field_widget_form_alter() can no longer affect the whole widget for multi-value fields

lyalyuk’s picture

Here is test for patch in #16. Please review.

yongt9412’s picture

Status: Needs work » Needs review
yongt9412’s picture

Let's trigger test bot.

Status: Needs review » Needs work
lyalyuk’s picture

jonathanshaw’s picture

Status: Needs work » Needs review

Setting to NR to trigger bot.

Status: Needs review » Needs work
lyalyuk’s picture

Berdir’s picture

Thanks, but this seems to ignore #30, which points out that there are already existing hooks in core. I haven't seen a response to that yet that would explain why we would need to custom hooks, which are basically identical as far as I see.

lyalyuk’s picture

@Berdir
No I didn't skip #30. The main difference for subform hooks is performance. We have landing page nodes with 30+ paragraphs in field. And I feel our case is not the only.

hook_field_widget_form_alter() doesn't match because it is called too often.

hook_field_widget_WIDGET_TYPE_form_alter() is a little bit better in performance aspect but it still called ($delta + 1) times when you click on "edit" for separate paragraph item. Also it is called even when paragraphs subform is closed and when user collapse subform.
If we were use this hook we have to check if the widget is open.

So subform hook allows us to avoid unnecessary checks and function calls. I think these hooks are have sense for big quantity of paragraphs.

sgurlt’s picture

I also just had a look on that and I have to agree with Iyalyuk.
hook_field_widget_form_alter() just called way to often when you look on entities edit pages with multiple paragraphs and it could have a bad influence on the performance when we are talking about sites with 40+ nested paragraphs.

@Berdir: I agree with you that it would be possible to just solve it using hook_field_widget_form_alter() or hook_field_widget_WIDGET_TYPE_form_alter(), but from performance point of view I really would like to go with the additional hooks. What do you think?

jonathanshaw’s picture

Aren't there things you can't do from within a widget hook, like showing/hiding a field conditionally on another one?

Berdir’s picture

Maybe there was a misunderstanding with the widget hook? Each paragraph form is also a widget and it's actually one level *up* from the subform alter. Meaning, you can implement hook_field_widget_paragraphs_form_alter(&$element, FormStateInterface $form_state, $context) and then the paragraph subform is in $element['subform'].

So I don't see where the performance argument is coming from, this issue actually adds 4 hook invocations per paragraph.

The main limitation with that is that you can't alter the add paragraph area, as that is outside of the item element, but as I said, neither do the hooks here allow that.

And for that there actually is #2940201: hook_field_widget_form_alter() can no longer affect the whole widget for multi-value fields now. If someone would be pushing that forward then I think it might still have a chance of landing in 8.5, which is not that far away anymore.

lyalyuk’s picture

@Berdit Hi! I did not have a chance check performance with profiler. My comment about performance is based on the quantity of xdebug breaks. I know that it is not the totally correct way to measure performance. Thank you very much for you detailed answer about new behavior of hook_field_widget_form_alter() in 8.5. But I have to let you know the reason why did we start the development with this hooks.

We have two modules what we really enjoy: paragraphs_browser and paragraphs_previewer. I like them both but they both extends InlineParagraphsWidget (paragraph classic) and I don't have a way to use them both. I had an idea to make Paragraph Experimental widget pluginable. From the one side it will allow us to use multiple modules for widget, but from another side it requires a lot of changes and potentially it can make future development more complex. But from the my point of view plugin system is more flexible hook system.

Can you tell me how do you see the evolution of paragraph widget (Experimental) in future?

Jacine’s picture

Just wanted to say that I found this issue after struggling to implement a simple States API setting on a Paragraphs field, to toggle the visibility of one field, based on another. IIRC, I think was having a problem with the location of the widget being in a different place in node_form vs. node_edit_form. I think I tried hook_field_widget_paragraphs_form_alter() at the time without success. I'm not sure if these hooks are needed or not, but they definitely made what I was trying to accomplish a lot easier, with cleaner code. Thanks for linking that issue @Berdir, I'll check it out.

miro_dietiker’s picture

I‘d vote to add an example to Paragraphs tests or maybe demo that demonstrate widget alteration and how to use states correctly.

For easier states handling we once discussed to add generator methods to the widget so there is less guessing. Lost track of this proposal, can‘t find a dedicsted issue. Let‘s figure out if this is still needed with the example?

EDIT Can be a separate issue as well..

Berdir’s picture

#2940201: hook_field_widget_form_alter() can no longer affect the whole widget for multi-value fields was just committed, so that's in 8.5 and available soon.

I still really don't understand the argument for these additional hooks.

The new hooks here are added in \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::formElement(), towards the end, which is called through \Drupal\Core\Field\WidgetBase::formSingleElement(), which then invokes the hooks that I mentioned.

That means it is called slightly more often, specifically also for paragraph items that are not in the edit mode, but I very much doubt that there is a measurable performance difference because a hook that simply checks for a non-empty $element['subform'] should be very fast. And adding completely new hooks might actually be slower because Drupal needs to check each installed module to see who implements the hook and cache that.

@Jacine: Can you share your hook implementation and what the problem was with implementing the existing hook? As I mentioned, I believe one problem with the current hook is that it is challenging to to get at certain information like the paragraph type, but that's something that we can fix with a single line $element['#paragraph_type'] = ...;.

Another thing is that if we know about specific problems/challenges is that we can document examples and how to get certain information in that hook.

Jacine’s picture

Thanks @Berdir! Yes, now that I think about it, I believe the lack of access to the Paragraph bundle is probably the reason I abandoned the hook_field_widget_WIDGET_TYPE_form_alter()approach. I need it, and couldn’t (and still can’t) seem to find it within that hook, and my attempts in hook_form_alter() were futile, so I was very happy to find this patch. LOL. This is the code I have in place:

function module_form_paragraphs_subform_hero_alter(array &$subform, FormStateInterface $form_state, $delta) {
  if (!empty($subform['field_heading'])) {
    $subform['field_heading']['#states'] = [
      'visible' => [
        ':input[name*="[field_hero_page_title]"]' => [
          ['checked' => FALSE],
        ]
      ]
    ];
  }
}

I went back to the code and tried it again with hook_field_widget_WIDGET_TYPE_form_alter(), and it does work, so it's probably not related to the core bug in my case. I still need to be able to limit to a specific bundle, but as you mentioned, adding a key for that would solve my problem for this use case.

function module_field_widget_paragraphs_form_alter(&$element, FormStateInterface $form_state, $context) {
  // Still need a way to limit this to bundle type.
  if (!empty($element['subform']['field_heading'])) {
    $element['subform']['field_heading']['#states'] = [
      'visible' => [
        ':input[name*="[field_hero_page_title]"]' => [
          ['checked' => FALSE],
        ]
      ]
    ];
  }
}
AndersNielsen’s picture

hook_field_widget_WIDGET_TYPE_form_alter() seemed to do the trick for me (on 8.4.4) thanks @Berdir for that tip
+1 for adding $element['#paragraph_type'] that could be very useful

function mymodule_field_widget_entity_reference_paragraphs_form_alter(&$element, &$form_state, $context) {
  if(!empty($element['subform']['field_toggle_display'])) {
    $delta = $context['delta'];
    $element['subform']['field_body']['#states'] = [
      'visible' => [
        ':input[name="field_paragraphs[' . $delta . '][subform][field_toggle_display]"]' => ['value' => 'body']
      ],
    ];
    $element['subform']['field_link']['#states'] = [
      'visible' => [
        ':input[name="field_paragraphs[' . $delta . '][subform][field_toggle_display]"]' => ['value' => 'link']
      ]
    ];
  }
}