Skip to main content

Select Element, its more hard to deal with Ajax

Drupal Form Select element is harder to deal with when desired to add to it Ajax activities

Ajax is a bit misleading, it's not all on the client side, and if we still want to handle it from the code which is located on the server, e/s program it with the form objects like $form & $form_state it still needs to update the server side within the flow, and the form is still need to have synchronization transactions between the client, server, and cache

not enough documentation on that, especially when working with specific elements like SELECT

My special case started when desire was to create form with arguments, so I will have the ability to initialize variable with in the form in each call, 

public function buildForm(array $form, FormStateInterface $form_state, $dashboard = 0, $project=0, $sprint=0, $redirect='')

but all argument are optional, sprint and projects are sub parts within dashboard, so if not given any arguments, user will have to select dashboard and then select the project and sprint.

the all form will be popped-up as an overlay window.

I use the Form select element to select the desired dashboard, which I added into its array the ajax section:

        '#ajax' => [
          'callback' => '::submitFormDashboardSelected',
          'wrapper' => 'group-select-wrapper',
          'suppress_required_fields_validation' => TRUE,
        ],  

I found out that in order for it to activate the callback function every time, and only at first time, I need to define it correctly, here are my tips:

  1. Don't define '#name'
  2. Don't define '#attribute'->id
  3. Better have the callback as static function: 'callback' => '::submitFormDashboardSelected'

here is the full select definition:

      $form['dashboard_select'] = [
        '#type' => 'select',
        '#title' => t('Dashboard'),
        //'#name' => 'dashboard-select',  <-- prevent the form_state to update
        '#options' => $dashboard_options,
        '#sort_options' => TRUE,
        '#required' => TRUE,
        '#empty_option' => "-- SELECT --",
        '#size' => 1,
        '#default_value' => $dashboard_default,
        '#attributes' => [
          'class' => ['use-ajax'],
          //'id' => "edit-dashboard-select", <-- this prevent the callback
        ],         
        '#ajax' => [
          'callback' => '::submitFormDashboardSelected',
          'wrapper' => 'group-select-wrapper',
          'suppress_required_fields_validation' => TRUE,
        ],        
      ];

 

 

Since my desire that selecting a dashboard will update the project and sprint selection boxes, I have put both of them under 'Field Group' element

      $form['group'] = [
        '#type' => 'fieldgroup',
        '#prefix' => '<div id="group-select-wrapper">',
        '#suffix' => '</div>',         
      ];

When selecting a dashboard, it will rewrite the project list and sprint list

both lists are shown in all cases even if argument has a sprint/project definition

the static callback method was as follows

    public static function submitFormDashboardSelected(array &$form, FormStateInterface $form_state) {
     $dashboard_id = $form_state->getValue('dashboard_select');      
     
     $options[0] = "-- select --";
     $form['group']['project_select']['#default_value'] = 0;
     $form['group']['project_select']['#value'] = 0;
     $form['group']['project_select']['#limit_validation_errors'] = [];
     $form['group']['sprint_select']['#default_value'] = 0;
     $form['group']['sprint_select']['#value'] = 0;
     $form['group']['sprint_select']['#limit_validation_errors'] = [];      
     if ($dashboard_id>0) {        
       $project_options = DashboardController::getProjects($dashboard_id); 
       $project_options_2 = $options + $project_options; //array_merge($options,$project_options );
       $form['group']['project_select']['#options'] = $project_options_2;
       
       $sprint_options = DashboardController::getSprints($dashboard_id);
       $sprint_options_2 = $options + $sprint_options ; //array_merge($options,$sprint_options );
       $form['group']['sprint_select']['#options'] = $sprint_options_2; 
     } 
     else {
       $form['group']['project_select']['#options'] = $options;
       $form['group']['sprint_select']['#options'] = $options;
     }
     return $form['group'];
   }

 

Few tips on that:

  1. empty option in select is not fully implemented and better to add 0 option
  2. to merge arrays is better to use: array+array and not array_merge, this will keep the array item keys
  3. the callback return the updated form part and not AjaxResponse

 

Final part, in order for it to work correctly we must be sure that core ajax library are loaded, we can do it by defining the libraries as follow in the buildForm method

      $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
      $form['#attached']['library'][] = 'core/drupal.ajax';
      $form['#attached']['library'][] = 'core/jquery.form';

 

I also added two buttons, one that cancel the operation and closing to overlay window

    public static function submitFormClose(array &$form, FormStateInterface $form_state) {

      $command = new CloseModalDialogCommand();
      $response = new AjaxResponse();
      $response->addCommand($command);
      return $response;
    }

 

the second, activate the actual function according to selections

    public static function submitFormCreate(array $form, FormStateInterface $form_state) {
      $dashboard_id = $form_state->getValue('dashboard_select');
      $project_id = $form_state->getValue('project_select');
      $sprint_id = $form_state->getValue('sprint_select');
            
      self::_createTask($form_state);
      
      $response = new AjaxResponse();
      $response->addCommand(new CloseModalDialogCommand()); 
      return $response;
    }

The from_state -> getValue of select element, returns the key of the selected item

don't forget the pre-define the PHP file with the libraries needed

  use Drupal\Core\Form\FormBase;
  use Drupal\Core\Form\FormStateInterface;
  use Drupal\node\Entity\Node;
  use Drupal\drupalvip_dashboard\Controller\DashboardController;
  use Drupal\Core\Ajax\CloseModalDialogCommand;
  use Drupal\Core\Ajax\AjaxResponse;

 

    public function buildForm(array $form, FormStateInterface $form_state, $dashboard = 0, $project=0, $sprint=0, $redirect='') {  //
      $config = \Drupal::config('drupalvip_task.settings');
      $log = $config->get('log');
      ($log)? \Drupal::logger("drupalvip_task")->debug(__FUNCTION__ . ": START "):'';      
      ($log)? \Drupal::logger("drupalvip_task")->debug("Form Args: da[$dashboard] pr[$project] sp[$sprint] rd[$redirect]"):'';
      
      $form['redirect'] = array(
          '#type' => 'value',
          '#value' => $redirect,
      );
      
      //dashboard select
      $dashboard_default = ($dashboard==0) ? null : $dashboard;
      $dashboard_options = DashboardController::getDashboards();
      $form['dashboard_select'] = [
        '#type' => 'select',
        '#title' => t('Dashboard'),
        //'#name' => 'dashboard-select',  <-- prevent the form_state to update
        '#options' => $dashboard_options,
        '#sort_options' => TRUE,
        '#required' => TRUE,
        '#empty_option' => "-- SELECT --",
        '#size' => 1,
        '#default_value' => $dashboard_default,
        '#attributes' => [
          'class' => ['use-ajax'],
          //'id' => "edit-dashboard-select", <-- this prevent the callback
        ],         
        '#ajax' => [
          'callback' => '::submitFormDashboardSelected',
          'wrapper' => 'group-select-wrapper',
          'suppress_required_fields_validation' => TRUE,
        ],        
      ];      
      
      $form['group'] = [
        '#type' => 'fieldgroup',
        '#prefix' => '<div id="group-select-wrapper">',
        '#suffix' => '</div>',         
      ];      
        $options[0] = "-- select --";        
        $form['group']['project_select'] = [
          '#type' => 'select',
          '#title' => $this->t('Project'),
          '#options' => $options,
          '#limit_validation_errors' => [],
          //'#empty_option' => "-- select --",
          '#size' => 1,        
          '#default_value' => 0, 
        ]; 
        if ($dashboard > 0) {
          $project_options = DashboardController::getProjects($dashboard);
          $project_options_2 = $options + $project_options ;
          $form['group']['project_select']['#options'] = $project_options_2; //$project_options; 
          $form['group']['project_select']['#default_value'] = $project;
        }      

        $form['group']['sprint_select'] = [
          '#type' => 'select',
          '#title' => $this->t('Sprint'),
          '#options' => $options,
          '#limit_validation_errors' => [],
          //'#empty_option' => "-- select --",
          '#size' => 1,        
          '#default_value' => 0, 
        ]; 
        if ($dashboard > 0) {
          $sprint_options = DashboardController::getSprints($dashboard);
          $sprint_options_2 = $options + $sprint_options ;
          $form['group']['sprint_select']['#options'] = $sprint_options_2; 
          $form['group']['sprint_select']['#default_value'] = $sprint;
        }      
        
      $form['subject'] =  [
        '#type' => 'textfield',
        '#title' => $this->t('Subject'),
        '#required' => TRUE,
        '#attributes' => [
          //'id' => "task-subject-box",
        ],        
      ];
      
      $form['actions'] = [
        '#type' => 'actions',
        '#attributes' => [
          'class' => ['form_actions'],
        ],         
      ];
        $form['actions']['cancel'] = [
          '#type' => 'submit',
          '#value' => $this->t('Cancel'),
          '#attributes' => [
            'class' => ['button','use-ajax','form-submit-modal','action-cancel' ],
          ],
          '#ajax' => [
            'callback' => [$this, 'submitFormClose'],
            'event' => 'click',
          ],
        ];       
        $form['actions']['create'] = [
          '#type' => 'submit',
          '#value' => $this->t('Create'),
          '#attributes' => [
            'class' => ['button','use-ajax','form-submit-modal','action-create' ],
          ],
          '#ajax' => [
            'callback' => [$this, 'submitFormCreate'],
            'event' => 'click',
            'onclick' => 'javascript:var s=this;setTimeout(function(){s.value="Saving...";s.disabled=true;},1);',
          ],
        ];              

      $form['#attributes']['class'][] = 'drupalvip_form';  
      $form['#attached']['library'][] = 'drupalvip_task/content';
      $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
      $form['#attached']['library'][] = 'core/drupal.ajax';
      $form['#attached']['library'][] = 'core/jquery.form';
      
      ($log)? \Drupal::logger("drupalvip_task")->debug(__FUNCTION__ . ": RETURN FORM "):'';
      return $form;    
    }