2

I'm struggling with coding an ajax refresh of options in a select list based on what's already selected in the previous field.

Scenario is I get a list of positions and each position one or many nominations for that position. So I display the position name and then a list of each nomination name which has a selection listbox "1st preference", "2nd preference", etc and the preferences are dictated by the number of nominations for that position.

So when someone selects for example "3rd preference" for the first nomination, I want to remove that "3rd preference" option from the select box of the next nomination's listbox. Any clues and guidance on this much appreciated. The following code is what I'm using in the tmpl/default.php.

<form id="form-voter"
  action="<?php echo Route::_('index.php?option=com_mycomponent&task=voterform.save'); ?>"
  method="post" class="form-validate form-horizontal" enctype="multipart/form-data">

<?php echo $nominations->form->renderFieldset('sysinfo'); ?>
<?php echo $nominations->form->renderField('user_id'); ?>
<?php echo $nominations->form->renderField('cat_id'); ?>

<?php echo LayoutHelper::render('default_proxy', array('data' => $nominations), dirname(__FILE__)); ?>

<?php /* ------------  Cycle through the positions records and show the nomination   ------ */ ?>
<?php /* the nominations are in an array of position arrays and then each position array is */ ?>
<?php /* an array of nominations objects. so Key is the posID */ ?>

<?php foreach ($nominations->nominations AS $posID => $nomins) : $NomCntr = 0; ?>

    <?php if (is_array($nomins) && !empty($nomins)) : ?>

        <div class="control-group">
            <div class="control-label">
                <label id="jform_nom_ids_lbl" for="jform_nom_ids_<?php echo $posID; ?>">
                    <?php echo $nomins[0]->position_id_name; ?>
                </label>
            </div>
            <div class="controls extrawide">

                <?php $qtyNoms = \count($nomins); ?>
                <?php foreach ($nomins AS $nom) : $NomCntr++; ?>

                    <?php if ($NomCntr == 1) : ?>
                        <?php echo '<p> <p /><p>'.Text::sprintf('COM_MYCOMPONENT_MULTIPLE_POSITIONS_PREF', $nom->pos_qty).'<p />'; ?>
                    <?php endif; ?>

                    <?php echo $selectList = MyajaxHelper::displayPrefVoteOptions($nom, $qtyNoms, $posID); ?>

                <?php endforeach; ?>

            </div>
        </div>
    <?php endif; ?>

<?php endforeach; ?>

<div class="control-group">
    <div class="controls">
        <?php if ($nominations->canSave): ?>
            <button type="submit" class="validate btn btn-primary">
                <?php echo Text::_('JSUBMIT'); ?>
            </button>
        <?php endif; ?>
        <a class="btn btn-secondary"
           href="<?php echo Route::_('index.php?option=com_mycomponent&task=voterform.cancel'); ?>"
           title="<?php echo Text::_('JCANCEL'); ?>">
            <?php echo Text::_('JCANCEL'); ?>
        </a>
    </div>
</div>

<input type="hidden" name="option" value="com_mycomponent"/>
<input type="hidden" name="task" value="voterform.save"/>
<?php echo HTMLHelper::_('form.token'); ?>

EDITED: since reviewing the original code I've updated the above and with a hope I may get an ajax answer, I've moved the selection list to a helper called MyajaxHelper and the code is:

    /**
 * Create a selection list for preferential voting
 * @param   object  $nom  nomination object
 * @param   integer $qtyNoms number of nominations for this position
 * @param   integer $posID  id reference of the position
 * @return  selection list box
 */
public static function displayPrefVoteOptions($nom, $qtyNoms, $posID)
{
    // Load admin language file
    $lang = Factory::getApplication()->getLanguage();
    $lang->load('com_mycomponent', JPATH_ADMINISTRATOR);

    // setup array of the votes per option
    $nomVotes = \range(1,$qtyNoms,1);
    \arsort($nomVotes);
    $i=1;

    $selectList = '<div class="selectlist">';
    $selectList .= '<label id="jform_nom_ids_lbl" for="jform_nom_ids_'.$posID.'" class="selectlist">';
    $selectList .= '<div style="width:230px;float:left;">'.$nom->nomination_name.'</div>';
    $selectList .= '<div style="with:100px;float:right;">';
    $selectList .= '<select id="jform_nom_ids_'.$posID.'" name="jform[nom_ids][nom_id_'.$posID.'_'.$nom->id.']">';

    foreach ($nomVotes as $vote) {
        $nf = new \NumberFormatter('en_GB', \NumberFormatter::ORDINAL);
        $sel = $nf->format($i);

        if ($i == 1) {
            $selectList .= '<option value="0" >'.Text::_('MYCOMPONENT_NO_VOTE').'</option>';
        }
        $selectList .= '<option value="'.$vote.'" >'.Text::sprintf('COM_MYCOMPONENT_PREF_SELECTION', $sel).'</option>';

    }
    $selectList .= '</select></div></label></div>';
    
    return  $selectList;
}
5
  • Using the unchanged posID in for or id attributes outside of the loop is okay, but inside of the loop looks like trouble because you will be producing invalid HTML via duplicated id values. That must change. I see where $NomCntr is incremented, but not where it is firat declared. I generally do not support building form field keys via concatenation of incremented values. Might you change: jform[nom_ids][nom_id_<?php echo $posID.'_'.$NomCntr; ?>] to jform[nom_ids][<?php echo $posID; ?>][]? That false;" after your option value looks like a typo. Commented Mar 11, 2023 at 7:18
  • Should that inner loop build that HTML markup just once BEFORE entering the outer loop? That would spare PHP from doing the same task N times. ...sorry if I am misreading your script; I am reviewing your code from my phone. Commented Mar 11, 2023 at 7:21
  • Thank Mick, yes with fresh eyes this morning I've removed the counter but I still need the concatenation to identify which position and nomination I'm addressing. And yes the false is a typo. Some edits to the code in the question coming. Commented Mar 12, 2023 at 6:08
  • The default interval of range() is 1 so that 3rd argument can be safely omitted. Since there are no answers, it is safe to remove the initial coding attempt if you wish (to reduce the question size). Why do you arsort() if you don't use the preserved keys? Maybe you want foreach (range($qtyNoms, 1) as $vote) {. Why use $i? Can you not simply unconditionally concatenate that option after the loop? Commented Mar 12, 2023 at 9:55
  • Yes that 3rd argument is there to remind me how the range works when looking at the code at a later date. The arsort() is to reverse the vote so if 4 preferences to choose from then 1st preference gets 4 votes, 2nd preference gets 3 etc. After waking in the wee smalls of the night I think you're right about the concatenation aspect and I may revisit this process again. Thanks for taking the time. Commented Mar 12, 2023 at 22:59

1 Answer 1

2

You put a lot of work into this already as I see. Since you preload the Form with all the options, you do not have to load the options again and again from PHP (from the server). So according to my approach and understanding of this task, you do not need an Ajax call for this at all.

What you need is a Javascript script that is updating the possible options (preferences pool) after one option was already selected for a Position.

I created an abstract of this problem with Javascript and HTML. I really hope I understood the explained task well:

The HTML Form:

<form action="" id="myform">
  <label for="cars">Choose a car:</label>
  <select name="cars" id="cars">
  <option value="">--Please choose an option--</option>
    <option value="volvo">Volvo</option>
    <option value="saab">Saab</option>
    <option value="opel">Opel</option>
    <option value="audi">Audi</option>
  </select>
  <br><br>
  <label for="ships">Choose a ship:</label>
  <select name="ships" id="ships">
  <option value="">--Please choose an option--</option>
    <option value="volvo">Volvo</option>
    <option value="saab">Saab</option>
    <option value="opel">Opel</option>
    <option value="audi">Audi</option>
  </select>
  <br><br>
  <label for="drones">Choose a drone:</label>
  <select name="drones" id="drones">
  <option value="">--Please choose an option--</option>
    <option value="volvo">Volvo</option>
    <option value="saab">Saab</option>
    <option value="opel">Opel</option>
    <option value="audi">Audi</option>
  </select>
  <input type="submit" value="Submit">
</form>

We can say, the Form provides different Selection sections but with the same options in all of the selection sections, so different positions but same nominees. The options are given, we do not have to load those again and again from the server in this case.

We just have to remove the already selected items from the possible options from the other select sections. And I just wrote a custom pure javascript code for this task. You have to load this JS script in your custom JS file on the page where your Form is:

(you have to change 'myform' Form id to your own 'form-voter' in this script)

window.addEventListener("load", () => {

    // add your form id to the selector!        
    const form1 = document.getElementById('myform');
    
    const selectEl = form1.querySelectorAll('select');     

    selectEl.forEach((selEl) => {
        
        selEl.addEventListener("change", () => {
            
            const opsCollection = selEl.options;
            
            let indexValue = selEl.options[selEl.selectedIndex];
            let textValue = indexValue.text;
            
            deleteEl(form1, textValue);
            addOption(selEl, textValue);                
        });
    });
});

function deleteEl (form, textValue) {
    const selectEl2 = form.querySelectorAll('select');
    for (let index = 0; index < selectEl2.length; index++) {
        const opsCollection = selectEl2[index].options;
        
        for (let i = 0; i < opsCollection.length; i++) {
                                    
            if (opsCollection[i].text == textValue  ) {            
                selectEl2[index].removeChild(opsCollection[i]);
            }
        };            
    }    
}

function addOption(el, value) {
    const opt = document.createElement('option');
    opt.value = opt.text = value;
    el.appendChild(opt);
    opt.selected = true;
}

And here you can check this whole script in operation:

https://jsfiddle.net/go4zdm6w/3/

Updated with using option field integer value: https://jsfiddle.net/9v7yde6r/1/

This script can theoretically handle as many select sections and options as you want. It could be optimized with a few lines here and there of course, but I think this is basically what you need. I really hope this will help you after seeing how hard work you already put into this.

Around 2 years ago I posted a longer write-up about how to load items dynamically in selection Form fields from the database in Joomla 3, using Ajax. But that is for a bit different task: How to dynamically reload admin-form-fields in Joomla

SECOND RUN

I updated the code with the bit deeper understanding of the task. Actually we only need the array from PHP, all other things can be handled by Javascript dynamically:

the HTML loaded with PHP:

<div class="container" class="created-form">
<h2>Position 1</h2>
    <div id="selection-area">
        <form action="" id="form3">
        <label for="nom-select">Choose a nominee:</label>
            <select></select>
        </form>
    </div>
</div>

And the Javascript part:

window.addEventListener("load", () => {

    // this array should be provided by PHP
    const positions = ['Foo', 'Bar', 'Baz', 'Gaz'];

    // this array should be provided by PHP
    const nominees = ["Select nominee", 'Adam', 'Stewart', 'David', 'Joe', 'Margaret'];

    const prefCount = nominees.length - 1 ;

    const createdForm = document.getElementById('form3');

    const select1 = createdForm.querySelector('select');

    let options = nominees.map(nominee => `<option value=${nominee}>${nominee}</option>`).join('\n');
    select1.innerHTML = options;

    let selectedPref = null;

    let selectedPrefTexts = [];    

    let counter1 = 0

    select1.addEventListener("change", (sel) => {

        counter1++;

        if (counter1 <= prefCount) {  

            const selValue = select1.options[select1.selectedIndex].text;

            let prefLabel = document.createElement("h4");
            prefLabel.innerHTML = "Select preference for " + selValue;            

            let prefSelect = document.createElement('select');
            createdForm.appendChild(prefLabel);
            createdForm.appendChild(prefSelect);

            const prefSelection = addOption(prefCount, prefSelect, createdForm, selectedPref, selectedPrefTexts);

            // we do not want the already selected nominee in options
            deleteNomOption(select1); 

            // disable nominees until preference will be selected for the selected nominee
            select1.setAttribute("disabled", "true");          
            
            prefSelection.addEventListener("change", () => {
                selectedPref = prefSelection.options[prefSelection.selectedIndex].text;
                selectedPrefTexts.push(selectedPref);
                
                let noticeNext = '<p id="short-notice" style="color: green; font-weigth: bold;">Select another nominee</p>';
                let thanksText = '<p id="thanks-notice" style="color: green; font-weigth: bold;">Thank you, please submit</p>'
                if (counter1 < prefCount) {
                    prefSelect.insertAdjacentHTML("afterend", noticeNext);                    
                } else {
                    prefSelect.insertAdjacentHTML("afterend", thanksText);
                }
               
                prefSelection.setAttribute("disabled", "true");

                // re-enable nominee select for the next nominee selection
                select1.removeAttribute("disabled");
            });
        }

        // adding a submit button when everything is selected
        if (counter1 == prefCount) {
            let subBtn = '<input type="submit" value="Submit" style="display: block; margin-top:0.8rem;">';
            createdForm.insertAdjacentHTML("beforeend", subBtn);

            // with click on submit we have to remove preferences' select fields 'disabled' attribute!!!!! -- ( element.removeAttribute("disabled") )
            // this function should be added here
            // or at every selection we should collect the selected data and submit that to the server as a prepared json object.
        }

    }); 

});

function addOption(count, el, form, selectedPref, selectedVals) {
    for (let index = 0; index < count+1; index++) {
            
        let prefOption = document.createElement('option');
        if (index == 0) {
            prefOption.innerHTML = "Select Preference"
            el.appendChild(prefOption);
            continue;
        }
        
        if (!selectedVals.includes("Preference " + (index))) {
            prefOption.value = "Preference " + (index);
            prefOption.innerHTML = "Preference " + (index);
            el.appendChild(prefOption);
        }      
                       
    }

    return el;
}

function deleteNomOption(el) {    
    el.options[el.selectedIndex].remove();
}

So this is the updated basic code of how I understood this task in structure and functionality. This could be still improved a lot with styling, and with little bit of modification here and there if we have much more time.

You can check and try this code in operation too on jsfiddle if it is reachable:

https://jsfiddle.net/tyvanmqf/1/

18
  • 1
    Thanks for taking the time with this Zollie, I will revisit and try to work through this as you suggested. This whole task has highlighted some serious holes in my knowledge, so thanks for helping. Commented Mar 12, 2023 at 23:01
  • 1
    Almost there, the problem is when you make a selection in the first position, that selection is not available in the other positions. So I have a position A where there are 2 nominees, so the two nominees are displayed and each has the dropdown list showing 1st Preference and 2nd Preference but then in position B which also has 2 nominees, I have no options to choose. so I need to separate for each position. I'll keep working on it. Commented Mar 13, 2023 at 0:21
  • 1
    I guess I need to change the listener to the div rather than the whole form? Commented Mar 13, 2023 at 0:27
  • 1
    your help in this is fantastic however my knowledge of javascript is nil to non-existent so I'm struggling to work through it. Having said that I'm still trying. I have another scenario where your first solution would suit but when an option is selected, the value is over-written by the selected text - instead of being <option value="1">Set as 4th preference vote</option> once selected it changes to <option value="Set as 4th preference vote">Set as 4th preference vote</option> so my number of votes is no longer an integer but string of text. I will try to implement second run. Thnx. Commented Mar 14, 2023 at 22:02
  • 1
    @GlennArkell - here is the updated first version, you can check: jsfiddle.net/9v7yde6r/1
    – Zollie
    Commented Mar 15, 2023 at 7:05

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.