2

I have been stuck on this for a few days now and could use some expert advice!

So, as a personal/learning project I'm making a fun little 'character relationship tracker' for my friends' D&D games. With this, I have a DB table of dummy dungeon masters as well as the game(s) they are running for the purpose of testing code. I am trying to create a cascading dropdown list generated from a mix of PHP and encoded to JSON as part of the submit new character form. It works! Except, in the database containing the info for the options, some have only one game in the list for a DM while others have multiple games for the same DM. For those singles, the JS is listing out every single letter as a different choice instead of just the single full option. I have tried remaking the PHP twice to try resolving this and shuffled through a handful of JS attempts to no avail. These changes have either broken the code completely or resulted in no changes.

This fiddle is what it is doing with my best attempt

as for the actual code:

HTML

<div class="selectdiv">
        <select name="chargm" id="charadm" onChange="chgm(this.value);" required>
            <option value="" selected="true" disabled>Game Master</option>
            <?php foreach ($gameMasters as $masterName) { echo "<option value='". $masterName . "'>" . $masterName . "</option>"; } ?>
        </select>
    </div>
    <div class="selectdiv">
        <select name="chargame" id="chargm" required>
            <option value="" selected="true" disabled>Game Name</option>
        </select>
    </div>

here is the PHP (I know it's super messy and redundant but I couldn't get it to work any other way for some reason and it does its job?)

//database connection stuff
$sqls = "SELECT * FROM datgames;";
$stmts = mysqli_query($conn, $sqls);
$resultcheck = mysqli_num_rows($stmts);
$gameMasters = array(); //create empty array of game masters.
$gameData = array(); //set up game list data array

//check if db can be read before contiuning.
if($resultcheck > 0){
  while($row = mysqli_fetch_assoc($stmts)){ //while there are rows, add to array
    $gameMasters[] = $row["datgamDM"]; //fill the gameMasters array with all gm's
    $gmdm = $row["datgamDM"]; //define gmdm as the game master of the row

    //copy existing info in gameData to preserve data
    $anotm = $gameData;

    //clear game data to reset it to avoid repeats
    unset($gameData);

    //create the key => value pair
    $tmpar[$gmdm] = $row["datgamName"];

    //merge the temp arrays and apply them to the global array
    $gameData = array_merge_recursive($anotm, $tmpar);

    //clear the temporary arrays to avoid repeats
    unset($anotm);
    unset($tmpar);
  }
}else{ exit(); } //if db can't be reached, break the code.

$gameMasters = array_unique($gameMasters);

//print_r($gameData); //making sure the array is right. this line is removed once this is working

and the exact JSON output from the PHP currently with this loop

{
    "Reid":[
        "Curse of Strahd",
        "ufkck"],
    "bob":[
        "Curse of Strahd",
        "fffs"]
    ,"jomama":"blaal",
    "taco":"salff"
};

and the JS adapted from divy3993's answer here

var list = <?php echo json_encode($gameData); ?>;

    function chgm(value) {
      if (value.length == 0) document.getElementById("chargm").innerHTML = "<option></option>";
      else {
        var games = "";
        for (categoryId in list[value]) {

          games += "<option>" + list[value][categoryId] + "</option>";
        }
        document.getElementById("chargm").innerHTML = games;
      }
    }

The question in short: What am I doing wrong in either PHP (most likely the cause) or Javascript that is causing the words in single-object groups to split into letters instead of showing the full word as the only option for the second drop down option?

Or rather, how do I get the PHP to make single-entries to show up as a multidimensional array while keeping the key so it shows up as an array in the JSON object?

5
  • json_encode($assocArrayOrObject); returns a JSON encoded String. Do like var list = JSON.parse(<?= json_encode($gameData); ?>); to convert the String to an Object. Of course, I would use the XMLHttpRequest to assign PHP data to a variable, but that's your call. Commented Feb 18, 2021 at 22:42
  • I do not recommend confusing your code with a variable name like $stmts when 1. It does not contain a prepared statement and 2. There is nothing "plural" about the value of the variable. Commented Feb 18, 2021 at 23:42
  • @StackSlave Thanks for the advice! Would XMLHttpRequest be better practice in than parsing it as an Object? Commented Feb 19, 2021 at 1:54
  • 1
    You would still be echo json_encode($objectOrAssocArray); from the separate PHP response page that you XMLHttpRequestInstance.send() to after XMLHttpRequestInstance.opening. You just have to remember that PHP executes on the Server before anything is sent to the Browser, so it's okay to build HTML pages with PHP, but after that you're going to want to use the XMLHttpRequest. Since you may be doing the same query after a page build you might as well just build the page with the XMLHttpRequest as well, depending on your needs. Commented Feb 19, 2021 at 2:07
  • 1
    JavaScript side may look like: const fd = new FormData; fd.append('test', 'neat'); const xhr = new XMLHttpRequest; xhr.open('POST', 'response.php'); xhr.responseType = 'json'; xhr.onload = function(){ const obj = this.response; console.log(obj.roundTrip); }; xhr.send(fd);. PHP may look like <?php $o = new StdClass; if(isset($_POST['test'])){ $test = $_POST['test']; if($test === 'neat'){ $o->roundTrip = $test; } } echo json_encode($o); ?>. Note that sometimes you will want to fd.append('property', JSON.stringify(someArray)); then $test = json_decode($_POST['property']); is an Array Commented Feb 19, 2021 at 2:18

2 Answers 2

1

The trouble with using array_merge_recursive() is that it can produce an inconsistent structure as it creates depth.

For instance, see that a 1st level key contains an indexed subarray if there is more than one element, but creates an associative array on the first level when only one element exists. I explain this here and provide a simple demonstration.

A result set from mysqli's query() is instantly traversable using foreach(), so I recommend that concise technique which sets up intuitive associative array accessing.

$result = [];
foreach ($conn->query("SELECT datgamDM, datgamName FROM datgames") as $row) {
    $result[$row["datgamDM"]][] = $row["datgamName"];
}
exit(json_encode($result));

This way, you have a consistent structure -- an associative array of indexed arrays. In other words:

{
    "Reid":["Curse of Strahd","ufkck"],
    "bob":["Curse of Strahd","fffs"],
    "jomama":["blaal"],
    "taco":["salff"]
}

Then life only gets easier. You only need to iterate like:

for (index in list[value]) {

As for the technique that you are using to generate the select/option markup -- that's not the way I would do it, there are multiple ways to do it, there are TONS of pages on StackOverflow that explain these options for you.

I generally don't like the UI of providing form instructions or labels as the top option of a field. I recommend that you give your form fields <label>s so that the options only contain real options.


As a completely different alternative, if you don't want to keep modifying the DOM every time the user makes a selection change, you could print ALL of the secondary select fields with their options preloaded, then "hide" them all. Then as the user changes the primary select field, merely "show" the field with the related id. This does of course create more html markup (which may or may not be attractive depending on your data volume), but it greatly reduces the complexity of the javascript code since all of the dynamic processing is done on page load. If one day, you wanted to make your primary select field a "multi-select", then having toggle-able secondary fields will work nicely. ..."horses for courses" and all that.

Sign up to request clarification or add additional context in comments.

1 Comment

1

don't take my comments too serious // is your old code /// are my comments

var list = {
    "Reid": ["Curse of Strahd", "uuck"],
    "bob": ["Curse of Strahd", "fffts"],
    "jomama": "blaal",
    "taco": "salff"
  };


  function chgm(value) {
  // if (value.length == 0) document.getElementById("chargm").innerHTML = "<option></option>";
  /// do not use == use ===
  /// avoid innerHTML
  var node = document.getElementById("chargm");
  var option;
  if (!value) {
      node.appendChild( document.createElement('options') );
          return;
  } 
    //else {
    /// pls do not append to a string!
    /// imagine you have 7000 values, the OS have everytime to get the string size AND 
    /// (re-)allocate new memory!!
    // var games = "";
    /// var games = []; // use an array instead of!
    /// use of instead of in, if you want to use in, you have to make sure if list.hasOwnProperty(value) is true
      // for (gameMas in list[value]) {
      /// for (var gameMas of Object.keys(list)) {
    /// but we know already what we are looking for, so let's check for that:
    if (list.hasOwnProperty(value)) {
        // ok we have now other new values, but what's with the other ones?
        // lets kill everything for lazyness
      while(node.firstChild && node.removeChild(node.firstChild));
      /// reset the header
         node.appendChild( document.createElement('option') ).innerText = 'Game / RP Name';
      // games += "<option>" + list[value][gameMas] + "</option>";
      /// your example array is inconsistent, so we have to check for the type
      if (!Array.isArray(list[value])) {
        /// with an array do this:
        /// games.push('<option>' + list[gameMas] + '</option>');
        
        option = node.appendChild( document.createElement('option') );
        option.innerText = list[value];
        option.value = list[value];
        return;
      }
      /// else
      for (var v of list[value]) {
        /// with an array do this:
        /// games.push('<option>' + list[gameMas][value] + '</option>');
        option = node.appendChild( document.createElement('option') );
        option.innerText = v;
        option.value = v;
      } 
    }
      // document.getElementById("chargm").innerHTML = games;
      /// with an array do this:
      /// document.getElementById("chargm").innerHTML = games.join('');
  }
<select name="chargm" id="charadm" onChange="chgm(this.value);">
  <option value="" selected="true" disabled="">DM/Admin</option>
  <option value="Reid">Reid</option>
  <option value="bob">bob</option>
  <option value="jomama">jomama</option>
  <option value="taco">taco</option>
</select>
<select name="chargame" id="chargm">
  <option value="" selected="true" disabled>Game / RP Name</option>
</select>

6 Comments

There is never, ever a reason to duplicate the option's text as its value value. If the two values are identical, never bloat your markup by declaring value.
if you don't set a text, it will have no text. There is no blowup, otherwise we have to check the innerText as value, or even more worst the innerHTML as value. Other than that, it's his HTML out of his fiddle. I just followed his structure
Always set the text. I am talking about not setting the value. Javascript will fetch the text as the value if the value is not declared. Here we see the painful cycle of people copy-pasting what someone else copy-pasted which was copy-pasted ...
if you have a formular, the name chargm have no value then, if you don't set a text, it will have no text. It doesn't matters about what you talking about, you are NOT right ^^ beside that, i copy and pasted HIS Code and made it working. I don't know what you are looking for here, but i'm here to exactly do that
@Comatose there is more than one reason, but for myself the most significant one is: if it's not chrome, it's faster not to use it. Beside that you might open the possibilty for XSS Attacks. The only reason for innerHTML should be Browser support, but this browsers nobody use anymore and in vanilla.js it's also not even implemented
|

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.