0

I have about a dozen string fields that contain objects and their states (for example, let's take 'AC_on' and 'heater_off'). And I have a view that shows nodes containing those fields. The task I'm trying to achieve is this. A user should have two sets of options to filter the nodes by the aforementioned fields:

  • To pick one of the objects via radio button (AC or heater);
  • To pick the states in which the previously picked option should be in the filtered set of nodes (only "on", or only "off", or "on" an "off" - so obviously check boxes).

It's no big deal to create the desired filters cosmetically: we can use combined fields filter for the set of radio buttons (search through all object fields and if the user picks "AC", filter out all "AC_null" values), and once again - for check boxes (for "On" include those values that contain "on" etc). The problem is, this approach doesn't work :) The second set doesn't care which object with"off" state it filters out.

So, I figured I could somehow pass whatever the user picked in the radio part (AC) into the values of the checkbox filter (so it wouldn't just search for "on", but for "AC_on"). The problem is, for the life of me I can't find a module that would allow me to do that.

"Views Dependent filters" seems like just the tool for the job, but unfortunately, it's for D7, and I have D9. Is there any way to achieve this? Any help would be highly appreciated.

PS I can't code, apart from the simplest PHP one-liners and CSS, so no-code solution is preferable.

2
  • Hi, welcome to Drupal Answers. Maybe you should have a look at this tutorial bouteillenicolas.com/fr/blog-drupal/…. Although it's for D7, the logic behind remains the same for D9 (plus, hook_form_alter() still exists in D9). I'm afraid you can't escape a bit of custom code, though.
    – misterdidi
    Feb 27, 2022 at 7:50
  • Hi @misterdidi! Thank you. I've already made my peace that no-code solution doesn't exist in this case. I've created my own module, but what kills me is that, more often than not, the code solutions proposed for D8 don't work on D9 (and D7 solutions almost never work). So, their adaptaion requires the knowledge I lack. This article you've sent looks exactly like the thing I need, I'll check it and come back to tell if it worked. Thanks again! Feb 28, 2022 at 8:37

2 Answers 2

1

Thank you misterdidi for setting me on a right track. The article you've provided helped, but in order to find out the solution, I had to study a few dozens more, and eventually the solution came around.

So, for this task the only certain solution I found is this:

  1. Read whatever is in "Radio filter".
  2. Alter "checkbox filter", so it would contain the value from "radio filter" in every option.
  3. Merge all "state fields" into one multi-value field, so we could...
  4. Use contextual filters to read "checkbox filter" value from URL and look for it in our multi-value field.

1. Reading value from Radio filter

I achieved this with views_pre_view hook:

function MY_MODULE_views_pre_view($view, $display_id, array &$args) {  
  // Check if it's the right view and display mode
  if ($view->id() == 'MY_VIEW') {
    switch ($display_id) {
      case 'MY_DISPLAY':
        //Get whatever value is in Radio Filter and write it into global variable
        $filter_input = $view->getExposedInput();
        global $arg;
        $arg= $filter_input['RADIO_FILTER'];
    }
  }
}

(Here and forward, everything IN CAPS is the names you have to replace with the ones you use)

With this code, I read what's in the Radio Filter and put it into a global variable $arg.

2. Put value from Radio Filter into Checkbox Filter

For this part I used form_views_exposed_form_alter hook:

function MY_MODULE_form_views_exposed_form_alter(&$form, $form_state, $form_id) {
  
  // Call the global variable with the value of Radio Filter
  global $arg;
  
  // Convert the raw value of the Radio Filter into something we can use for contextual filter
  if ($arg == '1') {
    $arg= 'AC'; 
  }
  elseif ($arg == '2') {
    $arg= 'HEATER'; 
  }
  elseif ($arg == 'All') {
    $arg= ''; 
  }
  
  $arg_on = ($arg . '_on');
  $arg_off = ($arg . '_off');
  
  // Check that it's the right form
  if ($form['#id'] == 'EXPOSED_FILTER_FORM_ID') {
    
    // We need different filter values, depending on whether value in Radio Filter is chosen, or if it's set to default ("All")
    if ($arg != '') {
    $form['CHECKBOX_FILTER']['#options'] = array(
      $arg_on => $arg_on,
      $arg_off => $arg_off,
      ); 
    }
    else {
      $form['CHECKBOX_FILTER']['#options'] = array(
      '_on' => '_on',
      '_off' => '_off', );
    }
    // If nothing is chosen, we should consider everything chosen.
    if ($form['CHECKBOX_FILTER']['#value'] == '') {
      $form['CHECKBOX_FILTER']['#value'] = [$arg_on, $arg_off];
    }
  }
}

This way I modify values in Checkbox Filter. Doing so makes them pretty much inoperable as exposed filters (I'm not sure why, but the moment the form is modified via hook - it doesn't filter anything anymore), but it provides URL parameters, which we will use for contextual filters.

3. Merging all parameter fields into one

This one is something I've expected to find a module for, but failed, so hook_entity_presave it is.

function MY_MODULE_entity_presave(Drupal\Core\Entity\EntityInterface $entity) {
  
  // Here we prepare variables for every field we will need to merge ...
  if ($entity->bundle() == 'MY_NODE_TYPE') {
    $par_ac = ($entity->field_AC_CHECK->value);
    $par_heater = ($entity->field_HEATER_CHECK->value);
    $par_all = ($par_ac . ',' . $par_heater);
  
    // ... and also check out if there's any "on" or "off" among them ...
    if (str_contains($par_all, 'on')) {
      $par_cont_on = '_on';
    }
    if (str_contains($par_all, 'off')) {
      $par_cont_off = '_off';
    }

    // ... to merge them all into one field, and provide the overall "on" and "off" values for the cases when Radio Filter is not filled
    $entity->field_par_comb = [$par_ac, $par_heater, $par_cont_on, $par_cont_off];
  }
}

I'm not particularly happy with the fact that the fields are hard-coded and not dynamically gathered, maybe later I'll think of a way of de-hardcoding these.

But for now, we have a multi-value field that contains a state for every parameter, and we can finally...

4. Use contextual filter

As we found out in p. 2, the exposed Checkbox Filter doesn't do anything when it's modified. But we can make it check the URL parameter of a Checkbox Filter. This way, if we have "checkbox_filter=heater_on", the contextual filter will look in our multi-value field from p. 3 for "heater_on" value.

Since Checkbox Filter can contain several values, we should choose "OR" conjunction in a filter's settings.

Note that the Radio Filter should be "not required". It's because, when the view is loaded for the first time, default exposed filter values are NOT shown in URL parameters, for some reason. But, since the default Radio Filter value is "Any" (and we should hide it with CSS via :first-child selector), in our code there's a part that takes this case into account by replacing "All" Radio Filter value with ''. And, since we have special check and "_on"/"_off" values in our multi-value field, it will work fine:

  • If the default "Any" value is chosen in Radio Filter, the code will not replace Checkbox Filter values.
  • In view settings, default Checkbox Filter values are set to "_on" and "_off" and the filter is operable.
  • When the user picks "AC", "Heater" or any other option in Radio Filter, it's being put into modified Checkbox Filter values and Contextual Filter kicks in.

This was a hell of a case for the first Drupal module development experience, but it works flawlessly. The customer is happy, I am happy.

-2

On the twig file

<script src="/themes/custom/customtheme/js/block-view.js"></script>

The js file

const cleanFields = (e) =>{
  document.querySelector('.form-item-field-type>.select-wrapper>select').value = 'All'
  document.querySelector('.form-item-field-1>.select-wrapper>select').value = 'All'
  document.querySelector('.form-item-field-2>.select-wrapper>select').value = 'All' 

}
const filterFields = (e) =>{
const deleteOption = document.querySelector('.form-item-type>.select-wrapper>select');
if(deleteOption.options[0].value == 'All'){
  deleteOption.remove(0)
}
if(e.value == '1'){
  document.querySelector('.form-item-field-1').style.display = 'none'
  document.querySelector('.form-item-field-2').style.display = 'none'
  return
}

if(e.value == 2){
  document.querySelector('.form-item-field-1').style.display = 'none'
  document.querySelector('.form-item-field-2').style.display = 'flex'
  return
}
if(e.value == 3){
  document.querySelector('.form-item-field-1').style.display = 'flex'
  document.querySelector('.form-item-field-2').style.display = 'none'
  return
}
}
const newSelect = (elem) => {
filterFields(elem);

elem.addEventListener('input', e =>{
    cleanFields(e.target)
    filterFields(e.target);
})

};

newSelect(document.querySelector('.form-item-type>.select-wrapper>select'));

(function() {
var proxied = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.send = function() {
    //Here is where you can add any code to process the request. 
    //If you want to pass the Ajax request object, pass the 'pointer' below
    var pointer = this
    var intervalId = window.setInterval(function(){
            if(pointer.readyState != 4){
                    return;
            }
            //console.log( pointer.responseText );
            
            //Here is where you can add any code to process the response.
            //If you want to pass the Ajax request object, pass the 'pointer' below
            clearInterval(intervalId);
            
            nuevoSelect(document.querySelector('.form-item-type>.select-wrapper>select'))

    }, 1);//I found a delay of 1 to be sufficient, modify it as you need.
    
    return proxied.apply(this, [].slice.call(arguments));
};
})();
1
  • This answer does not use Drupal APIs to reference the JavaScript file or to attach and load the JavaScript code.
    – cilefen
    Aug 3, 2022 at 15:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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