Skip to main content

Conditional Fields in Paragraphs Using the Javascript States API for Drupal 8

While creating content, there are pieces of information that are only relevant when other fields have a certain value. For example, if we want to allow the user to upload either an image or a video, but not both, you can have another field for the user to select which type of media they want to upload. In these scenarios, the Javascript States API for Drupal 8 can be used to conditionally hide and show the input elements for image and video conditionally.

Note: Do not confuse the Javascript States API with the storage State API.

The basics: conditional fields in node forms

Let’s see how to accomplish the conditional fields behavior in a node form before explaining the implementations for paragraphs. For this example, let’s assume a content type has a machine name of article with three fields: field_image, field_video, and field_media_type. The field_image_or_video field is of type List (text) with the following values: Image and Video.

/**
 * Implements hook_form_alter().
 */
function nicaragua_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if ($form_id == 'node_article_form' || $form_id == 'node_article_edit_form') {
    $form['field_ image']['#states'] = [
      'visible' => [
        ':input[name="field_image_or_video"]' => ['value' => 'Image'],
      ],
    ];

    $form['field_ video']['#states'] = [
      'visible' => [
        ':input[name="field_image_or_video"]' => ['value' => 'Video'],
      ],
    ];
  }
}

Note that in Drupal 8, the node add and edit form have different form ids. Hence, we check for either one before applying the field states. After checking for the right forms to alter, we implement the fields’ states logic as such:

$form[DEPENDEE_FIELD_NAME]['#states'] = [
  DEPENDEE_FIELD_STATE => [
    DEPENDENT_FIELD_SELECTOR => ['value' => DEPENDENT_FIELD_VALUE],
  ],
];

DEPENDENT_FIELD_SELECTOR is a CSS selector to the HTML form element rendered in the browser. Not to be confused with a nested Drupal form structure.

Conditional fields in Drupal 8 paragraphs

Although hook_form_alter could be used in paragraphs as well, their deep nesting nature makes it super complicated. Instead, we can use hook_field_widget_form_alter to alter the paragraph widget before it is added to the form. In fact, we are going to use the widget specific hook_field_widget_WIDGET_TYPE_form_alter to affect paragraphs only.

For this example, let’s assume a content type has a machine name of campaign with an entity reference field whose machine name is field_sections. The paragraph where we want to apply the conditional logic has a machine name of embedded_image_or_video with the following fields: field_image, field_video, and field_image_or_video. The field_image_or_video field is of type List (text) with the following values: Image and Video.

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function nicaragua_field_widget_paragraphs_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
  /** @var \Drupal\field\Entity\FieldConfig $field_definition */
  $field_definition = $context['items']->getFieldDefinition();
  $paragraph_entity_reference_field_name = $field_definition->getName();

  if ($paragraph_entity_reference_field_name == 'field_sections') {
    /** @see \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::formElement() */
    $widget_state = \Drupal\Core\Field\WidgetBase::getWidgetState($element['#field_parents'], $paragraph_entity_reference_field_name, $form_state);

    /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
    $paragraph_instance = $widget_state['paragraphs'][$element['#delta']]['entity'];
    $paragraph_type = $paragraph_instance->bundle();

    // Determine which paragraph type is being embedded.
    if ($paragraph_type == 'embedded_image_or_video') {
      $dependee_field_name = 'field_image_or_video';
      $selector = sprintf('select[name="%s[%d][subform][%s]"]', $paragraph_entity_reference_field_name, $element['#delta'], $dependee_field_name);

      // Dependent fields.
      $element['subform']['field_image']['#states'] = [
        'visible' => [
          $selector => ['value' => 'Image'],
       ],
      ];

      $element['subform']['field_video']['#states'] = [
        'visible' => [
          $selector => ['value' => 'Video'],
        ],
      ];
    }
  }
}

Paragraphs can be referenced from multiple fields. If you want to limit the conditional behavior you can check the name of the field embedding the paragraph using:

$field_definition = $context['items']->getFieldDefinition();
$paragraph_entity_reference_field_name = $field_definition->getName();

If you need more information on the field or entity where the paragraph is being embedded, the field definition (instance of FieldConfig) provides some useful methods:

$field_definition->getName(); // Returns the field_name property. Example: 'field_sections'.
$field_definition->getType(); // Returns the field_type property. Example: 'entity_reference_revisions'.
$field_definition->getTargetEntityTypeId(); // Returns the entity_type property. Example: 'node'.
$field_definition->getTargetBundle(); // Returns the bundle property. Example: 'campaign'.

In Drupal 8 it is a common practice to use the paragraph module to replace the body field. When doing so, a single field allows many different paragraph types. In that scenario, it is possible that different paragraph types have fields with the same name. You can add a check to apply the conditional logic only when one specific paragraph type is being embedded.

$widget_state = \Drupal\Core\Field\WidgetBase::getWidgetState($element['#field_parents'], $paragraph_entity_reference_field_name, $form_state);
$paragraph_instance = $widget_state['paragraphs'][$element['#delta']]['entity'];
$paragraph_type = $paragraph_instance->bundle();

The last step is to add the Javascript states API logic. There are two important things to consider:

  • The paragraph widgets are added under a subform key.
  • Because multiple paragraphs can be referenced from the same field, we need to consider the order (i.e. the paragraph delta). This is reflected in the DEPENDENT_FIELD_SELECTOR.
$element['subform'][DEPENDEE_FIELD_NAME]['#states'] = [
  DEPENDEE_FIELD_STATE => [
    DEPENDENT_FIELD_SELECTOR => ['value' => DEPENDENT_FIELD_VALUE],
  ],
];

When adding the widget, the form API will generate markup similar to this:

<select data-drupal-selector="edit-field-sections-0-subform-field-image-or-video"
  id="edit-field-sections-0-subform-field-image-or-video--vtQ4eJfmH7k"
  name="field_sections[0][subform][field_image_or_video]"
  class="form-select required"
  required="required"
  aria-required="true">
    <option value="Image" selected="selected">Image</option>
    <option value="Video">Video

So we need a selector like select[name="field_sections[0][subform][field_image_or_video]"] which can be generated using:

$selector = sprintf('select[name="%s[%d][subform][%s]"]', $paragraph_field_name, $element['#delta'], $dependee_field_name);

By using $element['#delta'] we ensure to apply the conditional field logic to the proper instance of the paragraph. This works when a field allows multiple paragraphs, including multiple instances of the same paragraph type.

You can get the example code here.

Warning: Javascript behavior does not affect user input

It is very important to note that the form elements are hidden and shown via javascript. This does not affect user input. If, for example, a user selects image and uploads one then changes the selection to video and sets one then both the image and video will be stored. Switching the selection from image to video and vice versa does not remove what the user had previous uploaded or set. Once the node is saved, if there are values for the image and the video both will be saved. One way to work around this when rendering the node is to toggle field visibility in the node Twig template. In my session "Twig Recipes: Making Drupal 8 Render the Markup You Want" there is an example on how to do this. Check out the slide deck and the video recording for reference.

What do you think of this approach to add conditional field logic to paragraphs? Let me know in the comments.

Comments

2017 November 18
Dalin

Permalink

FYI no need to hook in on the

FYI no need to hook in on the widget level. There’s a hook provided by Paragraphs with which you can target the specific paragraph bundle. Using it will eliminate the need for most of those if statements and your arrays won’t be as deep. I can’t remembered the name of the hook and I’m on my phone.

2017 November 20
Justin Winter

Permalink

Nice One!

This type of functionality is critical for improving the Drupal UX problem. Thanks for sharing.

One note, your "slide deck" link seem to be broken.

2017 November 20
Justin Winter

Permalink

Nicely Done

This is great. More information like this is critical for solving the Drupal UX Problem

2017 November 20
Mauricio Dinarte

Permalink

Thanks Justin. I have fixed

Thanks Justin. I have fixed the link to the slide deck.

2017 November 20
Mauricio Dinarte

Permalink

Hi Dalin. I do not see a hook

Hi Dalin. I do not see a hook provided by the paragraph module that can do that. I am looking at paragraphs.api.php Could you please share it?

Also, I intentionally make the extra checks as demonstration of how to do it in case it were needed.

2017 November 30
Mauricio Dinarte

Permalink

Thanks for sharing Tom! It

Thanks for sharing Tom! It looks good and would make the process simpler.

2019 April 11
Barrett Langton

Permalink

Thanks for the article, it…

Thanks for the article, it was very helpful! From that patch posted above, you can now get the paragraph type in a much simpler way. All you have to do now is "$element['#paragraph_type'] == 'embedded_image_or_video'".

Also, another note, I don't believe that issue ever actually added those other hooks. They determined that they wouldn't provide enough use beyond using the existing "hook_field_widget_WIDGET_TYPE_form_alter" hook. The only commit that was added was to add the above #paragraph_type value: https://git.drupalcode.org/project/paragraphs/commit/b04d2ac

2019 June 11
Taote

Permalink

I've implemented this code…

I've implemented this code successfully but it only works when the dependee field value changes, not when the node is edited or when a new paragraph is created. Until I select a value in the dependee field manually, the dependent value is always visible.

How can I have those fields hidden when a value is already selected?

2019 June 25
Norman

Permalink

There is no JavaScript…

There is no JavaScript States API. You mean the Form API and the #states system is just one of Form API's many features. I'd suggest fixing this article's title.

2019 July 19
Jon

Permalink

I'm having the same issue as…

I'm having the same issue as Taote. The field is always visible until I interact with the dependee field.

 

Also, I get an error on the node edit form, unless I check if the dependent form is set:

Notice: Undefined index: #type in drupal_process_states() (line 600 of core/includes/common.inc).

2019 July 24
Taote

Permalink

Yes, that worked for me too!…

Yes, that worked for me too!! Thank you Jon.

2019 October 22
paul

Permalink

My control field is a select…

My control field is a select text list with 3 options

3row|3row
2row|2row
1row|1row

It should hide paragraph rows depending on what the user selects. I need to hide multiple fields. I created 3 variables

fields_to_control1
fields_to_control2
fields_to_control3

and a for loop to place the multiple fields in. Below is a snippet. When I run it. Only the last for loop works. "1row"? Should I get the value of the select list option and add ...
if value == 1 than run for loop 1
if value == 2 than run for loop 2
if value == 3 than run for loop 3
end

 

Code below

$fields_to_control1 = ['field_description_col_5','field_link_col_5','field_description_col_6','field_link_col_6','field_description_col_7','field_link_col_7','field_description_col_8','field_link_col_8','field_description_col_9','field_link_col_9','field_description_col_10','field_link_col_10','field_description_col_11','field_link_col_11','field_description_col_12','field_link_col_12','field_description_col_13','field_link_col_13','field_description_col_14','field_link_col_14','field_description_col_15','field_link_col_15','field_description_col_16','field_link_col_16'];

        // Controlled fields for 2 rows
        $fields_to_control2 = ['field_description_col_9','field_link_col_9','field_description_col_10','field_link_col_10','field_description_col_11','field_link_col_11','field_description_col_12','field_link_col_12','field_description_col_13','field_link_col_13','field_description_col_14','field_link_col_14','field_description_col_15','field_link_col_15','field_description_col_16','field_link_col_16'];

        // Controlled fields for 3 rows
        $fields_to_control3 = ['field_description_col_13','field_link_col_13','field_description_col_14','field_link_col_14','field_description_col_15','field_link_col_15','field_description_col_16','field_link_col_16'];

            // Set the state of the controlled fields for 3 rows
            foreach($fields_to_control3 as $field){
              $element['subform'][$field]['#states'] = [
                       'invisible' => [
                         $selector => ['value' => '3row'],
                     ],
                  ];
            }

            // Set the state of the controlled fields for 2 rows
            foreach($fields_to_control2 as $field){
              $element['subform'][$field]['#states'] = [
                       'invisible' => [
                         $selector => ['value' => '2row'],
                     ],
                  ];
            }

            // Set the state of the controlled fields for 1 rows
            foreach($fields_to_control1 as $field){
              $element['subform'][$field]['#states'] = [
                       'invisible' => [
                         $selector => ['value' => '1row'],
                     ],
                  ];
            }

2020 February 05
Steve

Permalink

Where does one type this…

Where does one type this code? 

2020 May 15
Colby

Permalink

This was sooooo helpful…

This was sooooo helpful. Thank you!!

 

2020 November 04
Ankitha Shetty

Permalink

Awesome blog! Thanks for the…

Awesome blog! Thanks for the blog... I tried the conditional_fields module for the Paragraphs field, but that didn't seem to apply. This hook worked for me. The only change I did was, in hook_field_widget_WIDGET_TYPE_form_alter(), replaced the WIDGET_TYPE is with entity_reference_paragraphs.

2021 June 28
Manuel Velasco

Permalink

Thank you so much Greetings…

Thank you so much
Greetings from Bolivia.

2022 October 21
Deeps

Permalink

This does not work if I have…

This does not work if I have add this to nested paragraphs. It only works if paragraphs reference field field_sections is part of node.

2023 February 27
NCAC

Permalink

Very useful, thank you !

Very useful, thank you !

Add new comment

The content of this field is kept private and will not be shown publicly.

Markdown

The comment language code.
CAPTCHA Please help us focus on people and not spambots by answering this question.