1

The fields below are part of the node/add node/edit forms of my custom node type.

enter image description here

The paragraph field_para_city contain one field_city. (I later need to add other fields in the paragraph).

The user can add multiple instances from field_para_city.

field_city has an allowed_values_function defined in a YML file, which returns all possible cities from all countries.

I created a class MyNodeTypeForm extends NodeForm, with a public function buildForm(). This function adds AJAX to field_country so that when it changes (i.e. the user chooses a country) it (re-)populates all the existing instances of field_city (inside the field_para_city instances) with corresponding list of cities (e.g., London and Cardiff for the UK).

In MY_MODULE.module, I added the following so that the above code is executed on the node/add and node/edit forms.

function MY_MODULE_entity_type_alter(array &$entity_types) {
  $handlers = $entity_types['node']->get('handlers');
  $handlers['form']['default']  = 'Drupal\MY_MODULE\Form\MyNodeTypeForm';
  $handlers['form']['edit']     = 'Drupal\MY_MODULE\Form\MyNodeTypeForm';
  $entity_types['node']->set('handlers', $handlers);
}

All works fine, except when the user add or remove instances of field_para_city ("Add city" or "Remove"), then all instances of the field_para_city are refreshed and field_city instances inside them are reset to the allowed values (all cities), ignoring the state of field_country.

The user would then need to re-select from field_country to get the correct cities again.

I found that although adding/removing a field_para_city triggered the above mentioned buildForm() function, the $form_state->getValue('field_country') was then empty, unlike when buildForm() is triggered through the field_country AJAX.

I tried to capture the state of field_country in the field_city's allowed_values_function, so that the allowed options are dynamically and directly set based on the user choice of country, but $form_state was not recognised there.


I know this is an incorrect approach!

What is the correct better approach to implement this simple use case?

5
  • A question, have you tried or are you using drupal.org/project/conditional_fields ? Feb 20 '19 at 12:38
  • I thought about it. It allows changing field value based on another field's value, but I actually need something beyond that: the dependent field options should be programmatically populated taking into account also some existing contents and settings in the website.
    – Orion
    Feb 20 '19 at 13:41
  • I believe buildForm() runs everytime the AJAX handler is called. Therefore, many people opt to have the complete logic in buildForm() and then the ajax handler simply returns the $form back without touching it (maybe setting $form_state->setRebuild(FALSE);) . This thread may have relevant info on ajax forms: drupal.stackexchange.com/questions/236942/…
    – cchoe1
    Feb 21 '19 at 14:00
  • I actually have a different problem. The AJAX re-builds the form after being triggered by changes in field_country here. But what I need is a mechanism to rebuild the form (or at least the dependent field) when a new dependent field is being added. So the trigger here is the creation of the dependent field, at which I need to access the value of the controlling field.
    – Orion
    Feb 21 '19 at 14:37
  • May I invite the experts to help answering this question?
    – Orion
    Feb 23 '19 at 10:57
0

I managed to make it work using a different approach; instead of overriding the NodeFrom, I used hook_field_widget_WIDGET_TYPE_form_alter and edited the subform array there.
Example:
/** * Implements hook_field_widget_WIDGET_TYPE_form_alter(). */ function MY_MODULE_field_widget_entity_reference_paragraphs_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { //Logic to exclude other Paragraph types if ($element['#paragraph_type'] !== 'YOUR_PARAGRAPH_TYPE'){ //e.g. cities return; } $element['subform']['field_places']['#states'] = [ 'visible' => [ [ //It worked for me with the ID of the select, I think you can use the name also '#edit-field-cities-0-subform-field-city' => array('value' => 'Cairo'), ] ] ]; } When I add another paragraph, the ajax is fired and the selection is still preserved.
I haven't tried with other Ajax fields present though.

4
  • Thanks for this. However, I'm not sure how it exactly solves the problem. In my question, the problem is when I add another paragraph that the allowed values (options) in the list in the existing paragraph are reset but the selection is actually preserved.
    – Orion
    Feb 28 '19 at 23:57
  • By the way, did you mean field_country instead of field_places? If so, I wasn't able to get the AJAX of the field_county to be fired..
    – Orion
    Feb 28 '19 at 23:58
  • Oh sorry, it seems I misunderstood your question. I tried applying a different approach with the same hook but it didn't work for me neither. The fields kept being deselected with addition of new field item. field_places is totally irrelevant to your issue. Sorry again Mar 1 '19 at 9:34
  • No problem! I really appreciate your answer though!
    – Orion
    Mar 1 '19 at 10:54
0

You can do it by setting unique id for each dependent select box & replace its options whenever the parent select box is changed by setting this unique id as ajax wrapper in parent

These are three important things to make it working

A. Set unique id for child select box (dependent)

B. On parent select box set unique id(same uniqueid which is set in child dropdown) as [#ajax][wrapper], use [#ajax][method] as html .

C. In the Ajax callback use HtmlCommand() as Ajax response .

<?php

 use \Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Ajax\HtmlCommand;
 use Drupal\Core\Ajax\AjaxResponse;
/**  
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().  
*/  
 function modulename_field_widget_entity_reference_paragraphs_form_alter(&$element, &$form_state, $context) {
 if ($element['#paragraph_type'] == 'country_select') {
  $form_state->setRebuild(FALSE);
// Generating Unique id for each state by using parents & delta  
$gen_id = 'edit-state-';
foreach ($element['subform']['field_state']['widget']['#parents'] as $value) {
   $gen_id .= is_integer($value) ? "-". $value : "";
}
$gen_id .= "-".$element['subform']['field_state']['widget']["#delta"];
$element['subform']['field_state']['widget']['#attributes']["id"] = $gen_id;
 // Add ajax 
 $element['subform']['field_country']['widget']['#ajax'] = [
      'callback' => 'update_state_dropdown', //ajax call back
      'event' => 'change',
      'method' => 'html', // html method
      'wrapper' => $gen_id, // unique id
      'progress' => [
            'type' => 'throbber',
             'message' => NULL,
            ],
      ];
  // Set default value options for state when the country_select  paragraph is loaded     
  $default_country = $element['subform']['field_country']['widget']['#default_value'];
  if(isset($default_country[0])) {
     $options = get_state_options_by_country($default_country[0]); // replace with you function
    $element['subform']['field_state']['widget']['#options'] = $options;
  }
 }
} 

// Ajax call back

function update_state_dropdown(array &$element, FormStateInterface $form_state) {
 $triggeringElement = $form_state->getTriggeringElement();
 $value = $triggeringElement['#value'];
 $options = get_state_options_by_country($value); // replace with you function
 $wrapper_id = $triggeringElement["#ajax"]["wrapper"];
 $renderedField = '';
 foreach ($options as $key => $value) {
   $renderedField .= "<option value='".$key."'>".$value."</option>";
 }
 $response = new AjaxResponse();
 $response->addCommand(new HtmlCommand("#".$wrapper_id, $renderedField));
 return $response;
}

as per link. it also have github link for source

0

Have you tried using the Business Rules module? It provides this functionality. You may need a patch from Add AJAX support to Paragraphs if it hasn't been merged yet.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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