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?

Comatose
  • 55
  • 4
  • `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. – StackSlave Feb 18 '21 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. – mickmackusa Feb 18 '21 at 23:42
  • @StackSlave Thanks for the advice! Would XMLHttpRequest be better practice in than parsing it as an Object? – Comatose Feb 19 '21 at 01:54
  • 1
    You would still be `echo json_encode($objectOrAssocArray);` from the separate PHP response page that you `XMLHttpRequestInstance.send()` to after `XMLHttpRequestInstance.open`ing. 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. – StackSlave Feb 19 '21 at 02: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 `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 – StackSlave Feb 19 '21 at 02:18

2 Answers2

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>
Scriptkiddy1337
  • 792
  • 4
  • 9
  • 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`. – mickmackusa Feb 18 '21 at 23:43
  • 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 – Scriptkiddy1337 Feb 18 '21 at 23:47
  • 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 ... – mickmackusa Feb 18 '21 at 23:48
  • 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 – Scriptkiddy1337 Feb 18 '21 at 23:49
  • @RenéDatenschutz This is incredibly helpful with everything commented out! I do want to ask though, Is there a reason to avoid innerHTML completely? – Comatose Feb 19 '21 at 01:55
  • 1
    @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 – Scriptkiddy1337 Feb 19 '21 at 03:03
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.

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
  • 1
    @Com Why is innerHTML not recommended? Here is some reading: https://stackoverflow.com/q/7476638/2943403, https://stackoverflow.com/q/53308130/2943403, https://stackoverflow.com/q/18502238/2943403, https://stackoverflow.com/q/11515383/2943403 – mickmackusa Feb 19 '21 at 02:41