scum
scum

Reputation: 13

MySQL/PHP/JS Dynamically Populated Drop Down Menu

I'm working on a DB/web based frontend, and have encountered an issue. First off, I have a form with a drop down menu containing a list of contracts. Upon selection of a contract, I'd like for the jobs associated with that contract (fetched from the MySQL DB) to populate a second drop down menu below the first.

I would have just had all the info in one menu, but an 8000 entry drop down menu is a little unwieldy.

My PHP and HTML are barely passable, but enough for my purposes, however my ECMA experience is limited to a little bit of ActionScript in Flash MX, many moons ago. I'd like to avoid using third party JS libraries (such as jQuery) if at all possible, and I don't mind writing more code. I just need to know whether this is doable, and a little shove in the right direction.

I'll shut up now, heres the form to fetch the contract ID (and the associated client), and the incomplete job menu.

<select name='idcontract' onchange=''> 
<!--fetch/display contracts/clients-->
    <?php
        include 'sqldb.php';
        $cntqres = mysqli_query($dbc, 'SELECT * FROM contract');
        while ($cntrow = mysqli_fetch_array($cntqres))
        {
            $cliqres = mysqli_query($dbc, "SELECT * FROM client WHERE idclient = '$cntrow[idclient]'");
            while ($clirow = mysqli_fetch_array($cliqres))
            {
                echo "<option value='$cntrow[idcontract]'>$cntrow[idcontract] $clirow[name]</option>";
            }
        }
    ?>
</select>
<select name='idjob'>
    <option value='NULL'>Please select a contract</option>
<!--here goes the magical piece of code I don't know how to write-->
</select>

Any help would be much appreciated.

Edit:

Here's the PHP called by FeatherAJAX:

<?php
    include 'sqldb.php';
    $cnt = mysqli_real_escape_string($dbc, $_GET['cnt']);
    $sql = "SELECT * FROM job WHERE idcontract='$cnt' ORDER BY job.idjob";
    $jqres = mysqli_query($dbc, $sql);
    $i = 1;
    while (($jrow = mysqli_fetch_array($jqres)) && ($i < count($jrow)))
    {
        echo "idjob=><option value='$jrow[idjob]' id='$jrow[idjob]'>Job-$i $jrow[part_desc]</option>";
        $i++;
    }
?>

Upvotes: 1

Views: 3855

Answers (1)

Tom
Tom

Reputation: 9137

First off, you may want to rewrite the chunk of code that produces the contract options. Looping through query results and performing another query for each record is inefficient. Based on your queries, you might be able to use this code, which does a single query and then generate the options based on that. (I had to use made-up column names in the ORDER clause. In general, you should always sort your recordset so that results are in a determinate order -- even if you don't care what that order is.

<select name="idcontract" id="idcontract"> 
<!--fetch/display contracts/clients-->
<?php
include 'sqldb.php';
$clients = mysqli_query($dbc, '         
    SELECT  ct.idcontract, ct.idclient, cl.name
    FROM    contract ct LEFT OUTER JOIN client cl ON ct.idclient = cl.idclient
    ORDER BY ct.contractname, cl.clientname
    ');

while ($client = mysqli_fetch_array($clients)) {
    echo "<option value=\"{$client[idcontract]}\">{$client[idcontract} {$client[name]}</option>";
}
?>
</select>
<select name="idjob" id="idjob">
    <option value="NULL">Please select a contract</option>
</select>

To your question, the code you're looking for actually doesn't go where that comment is. What you need is an event handler that responds to the user picking an option in the first SELECT; it should then grab the value of that option and request from the server a set of key-value pairs to stuff into the second SELECT.

Something like this:

document.getElementById('idcontract').onchange = function(event) {
    // grab currently selected value
    var sValue = null;
    for(var i = 0, imax = this.childNodes.length; i < imax; i++) {
        var eOption = this.childNodes[i]; // shorthand
        if(eOption.selected) {
            sValue = eOption.value;
            break;
        }
    }

    if(!sValue) return;

    // get the sub-options for this value
    getSubOptions(sValue, function(XHR) {
        // this code runs once the response comes back from the server
        var aPairs = [];
        var nlJobs = XHR.getElementsByTagName('jobs'); // assumptions #1 & #2: response is XML, includes <job> tag for each job

        // extract key-value pairs from XML
        for(var i = 0, imax = nlJobs.length; i < imax; i++) {
            var xJob = nlJobs[i]; // shorthand
            /*
                assumption #3: <job> tag has "id" property
                assumption #4: job name appears inside <job> tag
                assumption #4.5: you've got an abstraction layer that normalizes XML node interfaces so that "text" and "textContent" are folded into "textContent"
            */
            aPairs.push({ 'key': xJob.getAttribute('id'), 'value': xJob.textContent });
        }

        // given array of key-value pairs, rebuild select box
        var eJobs = document.getElementById('idjob');

        setOptions(eJobs, aPairs);
    });
}

function setOptions(eNode, aPairs) {
    if(!eNode || !eNode.nodeName || eNode.nodeName.toUpperCase() !== 'SELECT') return false;

    // empty SELECT of all options
    while(eNode.firstChild) {
        eNode.removeChild(eNode.firstChild);
    }

    // build up new nodes
    var eOpt = null;
    for(var i = 0, imax = aPairs.length; i < imax; i++) {
        eOpt = document.createElement('OPTION');
        eOpt.value = aPairs[i].key;
        eOpt.appendChild(document.createTextNode(aPairs[i].value));
        eNode.appendChild(eOpt);
    }
    return true;
}

Of course, this is missing an important piece: you need some kind of AJAX abstraction layer. You don't need to get that from a framework, and a good library for this can be less than 50 lines of code (e.g. see PPK's ajax script on quirksmode.org), but you absolutely need something. That layer will provide two benefits: (1) cross-browser compatibility; (2) syntactic sugar.

For example, the code above doesn't include the definition of getSubOptions. That's because the logic will vary based on the interface provided by your AJAX abstraction. The idea, though, is that you'll perform a GET request against a script you write that accepts arguments and returns data satisfying that request. In the code above, I pretended that the script you write will return properly-formed XML data, with a MIME type identifying it as such. Alternatively, you could use JSON (or JSONP), straight text (e.g. CSV-style data), or even raw HTML that you'll just insert into the page.

The benefit of using a full framework is that they all provide convenient ways of doing DOM manipulation (i.e. syntactic sugar again).

The bottom line: you can absolutely do this with a homegrown approach (and I'm proud to say I've done it myself). But it will take longer -- not just because it's less convenient, but also because you'll have to re-invent the wheel === finding and fixing bugs in your code instead of leveraging well-tested core components from some library.

EDIT: If you want to use JSON as a data interchange format instead of XML, you'd modify the response handler being passed to getSubOptions like so:

getSubOptions(sValue, function(XHR) {
     // this code runs once the response comes back from the server
    var aPairs = eval(XHR.responseText); // assumes JSON defines an array of key-value pairs

    // given array of key-value pairs, rebuild select box
    var eJobs = document.getElementById('idjob');

    setOptions(eJobs, aPairs);
});

And here's a sample of what that JSON might look like:

[ { key: '1234', value: 'Job #1' },
  { key: '2345', value: 'Job #2' },
  ...
];

In this example, the JSON structure conveniently mirrors the property names expected by setOptions; that said, key and value seem pretty inoffensive.

If you're set on using JSON for data, you may want to look into JSONP as a more secure alternative. It's real similar, but the design pattern is a little different from the anonymous callback technique above.

EDIT 2: Modified sample code for the responder:

<?php
include 'sqldb.php';
$cnt = mysqli_real_escape_string($dbc, $_GET['cnt']);
$sql = "SELECT * FROM job WHERE idcontract='$cnt' ORDER BY job.idjob";
$jqres = mysqli_query($dbc, $sql);
$i = 1;

// prepare the response
header('Content-Type: text/html');

while (($jrow = mysqli_fetch_array($jqres)) && ($i < count($jrow))) {
    echo "<option value=\"$jrow[idjob]\" id=\"$jrow[idjob]\">Job-$i ${htmlentities(jrow[part_desc])}</option>";
    $i++;
}
?>

Upvotes: 2

Related Questions