0

I am trying to build a dropdown that has child and grandchild level objects that i need to access that are not included in the initial response of an ajax call.

I'm using this structure for my cascading ajax call:

$.ajax({
  url:"../../api/getEnum",
  data: {itemFilter: "", Id: "~some guid~"},
  dataType: "json",
  success: function(data){
   var dropdownObject = [];
   dropdownObject.push('<option value=""></option>');
   $(data).each(function(){
    //optgroup level
    if($(this)[0].HasChildren == true){
     dropdownObject.push('<optgroup label="' + $(this)[0].Text + '">');
     $.ajax({
      url:"../../api/getEnum",
      data: {itemFilter: "", Id: $(this)[0].Id},
      dataType: "json",
      success: function(data){
       $(data).each(function(){
        //prepend level
        if($(this)[0].HasChildren == true){
         var prependage = $(this)[0].Text;
         $.ajax({
          url:"../../api/getEnum",
          data: {itemFilter: "", Id: $(this)[0].Id},
          dataType: "json",
          success: function(data){
           $(data).each(function(){
            dropdownObject.push('<option value="' + $(this)[0].Id +'">' + prependage + ": " + $(this)[0].Text + '</option>');
           })
          }
         })
        }
       })
      }
     })
     dropdownObject.push('</optgroup>');
    }
    else {
     dropdownObject.push('<option value="' + $(this)[0].Id +'">' + $(this)[0].Text + '</option>');
    }
   });
  $('#dropDown').html(dropdownObject.join(''));
  $("#dropDown").trigger("chosen:updated"); //needs to be done to trigger the update of my drop down with "Chosen plugin" after all options have been built. 
  }
 })

Here's an example of the data result that i'm working with:

...{Id: "~some guid~", Text: "Medium", Name: "Medium", HasChildren: true,…}...

So if HasChildren is true, I need to cascade a new call to the API against the GUID of the parent. Due to the asynchronicity of AJAX, I think, the UI is only building the upper most level options that have no children. For performance, I don't want to turn off async but I don't know how to get my responses at the right time and in the right sequence.

I think I would probably need to build a callback function to handle the sequencing and then execute the update trigger at the end of that, but I don't have much experience with that and I'm not sure that's the right direction.

Thanks for the help!

chelseaCA
  • 11
  • 3

1 Answers1

-1

First of all, there are 2 statements I don't understand (lines 7-8):

var areaOption = [];
dropdownObject.push('<option value=""></option>');

Maybe the 1st one is useful elsewhere, so ok (though you'd probably better to put it outside of the success: function).
But I don't figure out how the 2nd one might be legitimate.

Source data structure.

Regarding the nested levels, I'm surprised you have 3 levels, while it seems that only 2 could do the job. When children exist you're building something like this:

<optgroup label="level-1-text">
  <option value="level-3-id">Level-2 text: Level-3 text</option>
</optgroup>

where the 2nd level is useful only to get "Level-2 text".
So schematically it appears that the corresponding source structure was like this:

calling Id: "~some guid~"
returns {Id: "level-1-id", Text: "Level-1 text", HasChildren: true}
>>> <optgroup label="level-1-text">
calling Id: "level-1-id"
returns {Id: "level-2-id", Text: "Level-2 text", HasChildren: true}
>>> (nothing)
calling Id: "level-2-id"
returns {Id: "level-3-id", Text: "Level-3 text", HasChildren: false}
>>> <option value="level-3-id">Level-2 text: Level-3 text</option>

So if you have hand on the source data structure you'd better to use something like this:

calling Id: "~some guid~"
returns {Id: "level-1-id", Text: "Level-1 text", HasChildren: "Level-2 text"}
>>> <optgroup label="level-1-text">
calling Id: "level-1-id"
returns {Id: "level-3-id", Text: "Level-3 text", HasChildren: false}
>>> <option value="level-3-id">Level-2 text: Level-3 text</option>

Note: here I keeped the "level-2" and "level-3" names for consistency, while there is only 2 levels now.

Usage of jQuery.

I was surprised you iterate the received JSON array with $(data).each(function(): this form is intended to process jQuery objects, while for arrays and for other, not-jQuery, objects we are supposed to use $.each(data, function().

So I had some tests and was surprised to discover it works too, though it someway should not! You can find the detailed investigation reported in the question I posted about that.

Another point is the way you address each item passed by the above iteration: $(this)[0].HasChildren is unnecessarily complicated, because its successively encapsulates this in a jQuery object then extracts it!
You should merely use this.HasChildren.

Suggested simple way to achieve the whole work.

Building HTML as a linear flux of strings is not an easy way, because:

  • the closing tag for <optgroup> must appear _after inner <option>s have been created
  • but the async mode of Ajax makes difficult to know when to do it

At the opposite it's easy to use jQuery to build <option>s and <optgroup>s each in turn when needed.
Note that it means we must use jQuery.append() instead of jQuery.html() to do that.

Regarding the nested aspect of the source data, the simplest way is to use recursion rather than using $.ajax several times in nested levels.
It also has the advantage of reducing the code and making it more readable.

Here is the snippet founded on all the above considerations.
Note that I also simplified it following my previous suggestion to merely use HasChildren (renamed Children for clarity) to provide the information about nested <optgroup>. For an <option> it not even has to be present.

function buildMenu(Id, $parent, prependage) {
  $parent = $parent || $('#dropdown');
  $.ajax({
    url:"../../api/getEnum",
    data: {itemFilter: "", Id: Id},
    dataType: "json",
    success: function(data) {
      $.each(data, function() {
        if (!this.Children) {
          // create a simple <option>
          $parent.append('\
<option value="' + this.Id + '">\
  ' + (!!prependage ? (prependage + ': ') : '') + this.Text + '\
</option>\
          ');
        } else {
          // create an <optgroup>
          $parent.append('\
<optgroup label="' + this.Text + '">\
</optgroup>\
          ');
          // call for <option>s in this optgroup
          buildMenu(this.Id, $('optgroup:last', $parent), this.Children);
        }
      });
    }
  })
}

buildMenu('~some guid~');

To be sure, I tested with a slightly modified version you can try to check it works as expected:

<?php
$menus = [
  '~some guid~' => [
    ["Id" => "level-1-1", "Text" => "Text-1-1"],
    ["Id" => "level-1-2", "Text" => "Text-1-2"],
    ["Id" => "level-1-3", "Text" => "Text-1-3", "Children" => "level-1-3"],
    ["Id" => "level-1-4", "Text" => "Text-1-4"],
    ["Id" => "level-1-5", "Text" => "Text-1-5", "Children" => "level-1-5"],
  ],
  'level-1-3' => [
    ["Id" => "level-1-3-1", "Text" => "Text-1-3-1"],
    ["Id" => "level-1-3-2", "Text" => "Text-1-3-2"],
    ["Id" => "level-1-3-3", "Text" => "Text-1-3-3"],
  ],
  'level-1-5' => [
    ["Id" => "level-1-5-1", "Text" => "Text-1-5-1"],
    ["Id" => "level-1-5-2", "Text" => "Text-1-5-2"],
    ["Id" => "level-1-5-3", "Text" => "Text-1-5-3"],
  ],
];
if ($Id = @$_REQUEST['Id']) {
  echo json_encode($menus[$Id]);
  exit;
}
?>
<select id="dropdown">
</select>
<script src="http://code.jquery.com/jquery-1.11.3.js"></script>
<script>
function buildMenu(Id, $parent, prependage) {
  $parent = $parent || $('#dropdown');
  $.ajax({
    url:'',
    data: {itemFilter: "", Id: Id},
    dataType: "json",
    success: function(data) {
      $.each(data, function() {
        if (!this.Children) {
          // create a simple <option>
          $parent.append('\
<option value="' + this.Id + '">\
  ' + (!!prependage ? (prependage + ': ') : '') + this.Text + '\
</option>\
          ');
        } else {
          // create an <optgroup>
          $parent.append('\
<optgroup label="' + this.Text + '">\
</optgroup>\
          ');
          // call for <option>s in this optgroup
          buildMenu(this.Id, $('optgroup:last', $parent), this.Children);
        }
      });
    }
  });
}

buildMenu('~some guid~');
</script>
Community
  • 1
  • 1
cFreed
  • 4,404
  • 1
  • 23
  • 33
  • May I know why this downvote? Everybody is free to think an answer is not pertinent, but he _should_ explain its advice! – cFreed Mar 16 '16 at 22:15
  • Thank you for the initial response! – chelseaCA Mar 18 '16 at 03:33
  • You are correct, when I was sanitizing I left a reference to "areaOption" and then referred to it as "dropdownObject" later on accident. I have updated my initial post for consistency. In my scenario, the initial call will result in some options and some optgroups that have child options. Some of those child options will, themselves, have children as well. Through console.log, I am able to see that all layers are being accessed. They're just joining before the data is available. Hope that clears it up! – chelseaCA Mar 18 '16 at 03:42
  • I worked on this for a bit and was able to use your guidance to apply the concept in a way that worked fabulous for me! Since i had valid options in each of the 3 levels I had to build the menu to treat each level option a particular way so I added a variable to the function to pass the level# for each parent: `level++: buildMenu(id, parent, prependage, level): level--;` And then I just logic'ed on what level I was building on. This really was a huge help. Thanks! – chelseaCA Apr 07 '16 at 16:52
  • @chelseaCA Glad to help. Thanks for the feedback! – cFreed Apr 08 '16 at 20:42