0

Trying to build a chained select form, I've done this when the I have a required number of selects using $.ajax requests using .on('change'), but things are starting to get out of control when I need to display more than 4 dropdown choices that may or may not exist based on prior selections. Is there a way to refactor this code using promises or something else. Maybe point me in the right direction? Here's an example code snippet.

$('#model').on('change',function(){


var modelID = $(this).val();
var yearsetID = $('#year').val();
var makesetID = $('#make').val();

if(modelID){
    $.ajax({
        url:'lib/ajaxData.php',
        type: "POST",
        data:{
            "action" : 'getsubmodels',
            "makeset_id" : makesetID,
            "yearset_id" : yearsetID,
            "model_id" : modelID
        },
        dataType: "html",
        success:function(html){
            if (html) {

               // found a submodel
               $('#submodel').html(html);

               $('#submodel').on('change',function(){
                            var submodelID = $(this).val();
                            var yearsetID = $('#year').val();
                            var makesetID = $('#make').val();
                            var modelsetID = $('#model').val();


                            $.ajax({
                                type:'POST',
                                url:'lib/ajaxData.php',
                                data: {
                                    "action" : 'getbodytypes',
                                    "year": yearsetID,
                                    "make" : makesetID,
                                    "model" : modelsetID,
                                    "submodel" : submodelID
                                },
                                success:function(html){


                                    // found a bodytype 
                                    if(html) {
                                            $('#bodytype').html(html);

                                            $('#bodytype').on('change',function(){
                                                //more dropdowns
                                            }


                                    }
                                }
                            });
                        });
                }else{

                            //no submodel

                        }
                    }
                }); 
            }else{
                $('.product').html('Select Some Values'); 
            }
        });

EDITED to show HTML

Here's what the HTML looks like, some dropdowns are hidden unless that option is available.

<div class="select-boxes row">
        <div class="small-12 medium-1 columns">
        <select name="year" id="year">
            <option value="">Year </option>

        </select>

        </div>
        <div class="small-12 medium-1 columns">
        <select name="make" id="make">
            <option value="">Make</option>
        </select>

        </div>
        <div class="small-12 medium-1 columns end">
        <select name="model" id="model">
            <option value="">Model</option>
        </select>
        </div>
        <div class="small-12 medium-1 columns end submodel modifier" style="display:none;">
        <select name="submodel" id="submodel">
            <option value="">Submodel</option>
        </select>
        </div>
        <div class="small-12 medium-1 columns end bodytype modifier" style="display:none;">
        <select name="bodytype" id="bodytype">
            <option value="">BodyType</option>
        </select>
        </div>
        <div class="small-12 medium-1 columns end enginetype modifier" style="display:none;">
        <select name="engine" id="engine">
            <option value="">EngineType</option>
        </select>
        </div>
        <div class="small-12 medium-1 columns end drivetype modifier" style="display:none;">
        <select name="drive" id="drive">
            <option value="">DriveType</option>
        </select>
        </div>
    </div>
Paully
  • 13
  • 5
  • 4
    Read about [event delegation](https://learn.jquery.com/events/event-delegation/#event-propagation). This will allow you to attach event handlers to dynamically created elements as you are doing. – Mikey Feb 15 '17 at 17:54
  • More info: http://stackoverflow.com/questions/203198/event-binding-on-dynamically-created-elements – freedomn-m Feb 15 '17 at 18:04
  • You could create a single function and use logic to set values to call at completion of `$.ajax()`, instead of nesting `$.ajax()` calls where parameters and `success` handler is essentially the same. – guest271314 Feb 15 '17 at 18:11
  • 1
    Given your requests don't depend on the selections, or ajax responses, of the previous dropdowns, there's no reason to nest anything here. – Bergi Feb 15 '17 at 18:34
  • Thanks for the suggestions, I'll look into event delegation. The requests are dependent on the previous dropdown selections, that's the dilemma I am having. So user selects dropdown A, ajax sends request to database to populate dropdown B, user selects B, ajax sends request to database to populate dropdown C, C is determined by the values of A and B, and down the line we go... – Paully Feb 15 '17 at 18:44

4 Answers4

1

More readable

$('#model').on('change', modelOnChange);

    function modelOnChange() {
      var modelID = $(this).val(),
          yearsetID = $('#year').val(),
          makesetID = $('#make').val();

      (modelID) ? doStuff(modelID, yearsetID, makesetID) : doStuffIfNoId();
    }

    function doStuff(modelID, yearsetID, makesetID) {
      getSubModels(afterGetSubModels);
    }

    function doStuffIfNoId() {
      $('.product').html('Select Some Values');
    }

    function getSubModels(callback) {
      $.ajax({
        url     : 'lib/ajaxData.php',
        type    : "POST",
        data    : {
          "action"    : 'getsubmodels',
          "makeset_id": makesetID,
          "yearset_id": yearsetID,
          "model_id"  : modelID
        },
        dataType: "html",
        success : function (html) {
          if (callback)callback(html);
        }
      });
    }

    function afterGetSubModels(html) {
      if (html) doStuffModels();
    }

    function doStuffModels() {
      // found a submodel
      $('#submodel').html(html);

      $('#submodel').on('change', subModuleChange);
    }

    function subModuleChange() {
      var submodelID = $(this).val(),
          yearsetID = $('#year').val(),
          makesetID = $('#make').val(),
          modelsetID = $('#model').val();

      getBodyTypes(afterGetBodyTypes);

      function getBodyTypes(callback) {
        $.ajax({
          type   : 'POST',
          url    : 'lib/ajaxData.php',
          data   : {
            "action"  : 'getbodytypes',
            "year"    : yearsetID,
            "make"    : makesetID,
            "model"   : modelsetID,
            "submodel": submodelID
          },
          success: function (html) {
            if (callback)callback(html);
          }
        });
      }

      function afterGetBodyTypes(html) {
        // found a bodytype
        if (html) {
          $('#bodytype').html(html);

          $('#bodytype').on('change', function () {
            //more dropdowns
          });

        }
      }
    }
1

Because #submodel already exists at the beginning of all this and isn't replaced, you can simply pull that even handler out of the nesting. It doesn't need to be inside. I'd also suggest you switch your ajax handling to using promises because it will allow you to prevent nesting on compound operations in the future.

It also appears the #bodytype already exists and is not being replaced either so you can do the same thing for it. Just pull the event handler out of the nesting.

If any of these elements are dynamically replaced or loaded, then you can use delegated event handling. I will show both versions of the code below.

Here's a version assuming that the #submodel and #bodytype are constant and not replaced (it's fine if their child html is replaced):

$('#model').on('change',function(){
    var modelID = $(this).val();
    var yearsetID = $('#year').val();
    var makesetID = $('#make').val();

    if (modelID) {
        $.ajax({
            url:'lib/ajaxData.php',
            type: "POST",
            data: {
                "action" : 'getsubmodels',
                "makeset_id" : makesetID,
                "yearset_id" : yearsetID,
                "model_id" : modelID
            },
            dataType: "html",
        }).then(function(html){
            if (html) {
                // found a submodel
                $('#submodel').html(html);

            } else {
                //no submodel
            }
        });
    } else {
        $('.product').html('Select Some Values'); 
    }
});


$('#submodel').on('change',function(){
    var submodelID = $(this).val();
    var yearsetID = $('#year').val();
    var makesetID = $('#make').val();
    var modelsetID = $('#model').val();

    $.ajax({
        type:'POST',
        url:'lib/ajaxData.php',
        data: {
            "action" : 'getbodytypes',
            "year": yearsetID,
            "make" : makesetID,
            "model" : modelsetID,
            "submodel" : submodelID
        }
    }).then(function(html){
        // found a bodytype 
        if(html) {
            $('#bodytype').html(html);
        }
    });
});

$('#bodytype').on('change',function(){
    //more dropdowns
});

And, if another of those elements are dynamically replaced, then here's a version using delegated event handling that will work with that. Delegated event handling uses a different form of jQuery's .on(). Instead of:

$(selector).on(event, fn);

it uses:

$(staticParentSelector).on(event, dynamicElementSelector, fn);

Technically, how this works is that it assigned the event handler to a static parent object (one that is not dynamically replaced). Then, it uses event bubbling (in which events that occur on children are bubbled up the parent hierarchy and offered to parent elements). When the event hits the parent element, the jQuery delegated event handling checks to see if that event originated on a child selector that we are interested in. If so, it fires the event handler. This works with dynamically created elements because no event handler is actually attached to the dynamically created element that you are interested in. Instead, events that occur on that dynamic element are caught when they bubble up to a parent and the event handler is fired from there.

We don't use delegated event handling all the time because it has a few drawbacks one should be aware of. It's a bit less CPU efficient because a parent may have to check a lot of bubbled events from a lot of different children just to find the one that it was looking for. And, you don't get to handle the event as early in its life cycle so if you're trying to prevent a default action (like prevent the submission of a form), it may be too late by the time the event bubbles up to a parent. And, not all events don't bubble (though most do). None of those issues seem like they'd be a problem for you so, here's your code using delegated event handling.

Since I don't know your HTML, I just picked document.body as the static element to attach the event handlers to. It is generally best to pick the closest parent element that does not get replaced so in your actual HTML, that would probably be a closer parent element.

$(document.body).on('change', '#model', function(){
    var modelID = $(this).val();
    var yearsetID = $('#year').val();
    var makesetID = $('#make').val();

    if (modelID) {
        $.ajax({
            url:'lib/ajaxData.php',
            type: "POST",
            data: {
                "action" : 'getsubmodels',
                "makeset_id" : makesetID,
                "yearset_id" : yearsetID,
                "model_id" : modelID
            },
            dataType: "html",
        }).then(function(html){
            if (html) {
                // found a submodel
                $('#submodel').html(html);

            } else {
                //no submodel
            }
        });
    } else {
        $('.product').html('Select Some Values'); 
    }
});


$(document.body).on('change', '#submodel', function(){
    var submodelID = $(this).val();
    var yearsetID = $('#year').val();
    var makesetID = $('#make').val();
    var modelsetID = $('#model').val();

    $.ajax({
        type:'POST',
        url:'lib/ajaxData.php',
        data: {
            "action" : 'getbodytypes',
            "year": yearsetID,
            "make" : makesetID,
            "model" : modelsetID,
            "submodel" : submodelID
        }
    }).then(function(html){
        // found a bodytype 
        if(html) {
            $('#bodytype').html(html);
        }
    });
});

$(document.body).on('change', '#bodytype', function(){
    //more dropdowns
});

In a separate part of the question, if you just want to reveal the next drop-down when a non-default value of a select is chosen, you can do that generically like this:

$(".select-boxes select").on("change", function(e) {
    // if we have a value here, then reveal the next select div
    if ($(this).val()) {
       // get parent div, then get next div, then show it
       $(this).closest(".small-12").next().show();
    }
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thank you for your detailed response, It helps a lot. I've added the HTML to original question. My major issue is getting the #bodytype to display if #model did not return a #submodel and so forth. – Paully Feb 15 '17 at 19:31
  • @Paully - That's a completely different problem that your question does not mention at all. You'd have to describe more what exact scenario you're trying to handle and what the problem is to solve there. And, stackoverflow really frowns upon situations where the question gets morphed into something completely different than was originally described, particularly after there are already a bunch of answers. Maybe you should take these code improvements and make a new question that talks about getting `#bodytype` to display in some particular circumstance? – jfriend00 Feb 15 '17 at 19:34
  • Thanks, I guess I was not clear, I mentioned that more dropdowns may or may not exist based on previous selections. I think my question was 2 parts, refactoring the code so it was easier to read and displaying dropdowns based on previous selections. I apologize if I was vague. Your examples are still very useful. – Paully Feb 15 '17 at 19:42
  • @Paully - I don't follow what the logic is for revealing subsequent drop-downs. After a choice is made in one, do you just want to reveal the next drop-down regardless of what the Ajax returned when this drop-down was selected? – jfriend00 Feb 15 '17 at 21:05
  • @Paully - If all you want to do is reveal the next select after selecting a value, I've added some code to the end of my answer that will do that. – jfriend00 Feb 15 '17 at 21:17
  • @ jfriend00 Thanks, this helps a lot, after reviewing some of your comments, I've decided that a lot of the logic could be handled server side, but your event delegation comments have led me in the right direction so far! Thanks again. – Paully Feb 15 '17 at 23:23
  • @Paully - Since it looks like you may be new here, let me explain that if this has answered your question, then you can indicate that to the community by clicking the green checkmark to the left of the answer to "accept" it. This will also earn you some reputation points for following the proper procedure here. – jfriend00 Feb 16 '17 at 00:07
  • Done! Thanks for the help! – Paully Feb 16 '17 at 14:00
0

The preferred method is to use deferred events rather than callback syntax. Introduced in jQuery 1.5 they make use of a syntax that's very similar the ES6 promises. Here's a tutorial on the topic by SitePoint.

In short, rather than passing in a callback function you'll chain the .done(), .fail(), .always(), and .then() methods onto a $.when call. The $.ajax() call is made as an argument to the $.when() method.

Here's an example:

$.when(
    // Make an AJAX request.
    $.ajax( .. )
    // The when block will return the jqXHR Object
    // You could also use the $.post() or $.get() methods here.
)
.done(
    // any code here will only run if the .when() request is successful
)
.fail(
    // Code here will only run if the .when() request fails
)
.always(
    // Code placed here will always run. Regardless of success or failure.
    // This is usually placed last for additional cleanup, etc. that is
    // performed whether the .when() returned success or failure.
)

Hope that this helps somewhat!

Joshua Kleveter
  • 1,769
  • 17
  • 23
  • 1
    Note, `jQuery.ajax()` returns a jQuery promise object, `$.when()` is not necessary for single `$.ajax()` call. – guest271314 Feb 15 '17 at 18:44
  • Thanks, I have been exhaustively looking into this method, but can't seem to figure out how this would work with multiple .on('change') events... – Paully Feb 15 '17 at 18:47
0

I see that you have a lot of nested ajax calls.

That's what we call Callback hell

I recommended you to use the Promise way with Ajax instead callback, but that will result in Promise hell too.

So, the only solution is using the ES2017 async/await syntax; so, change your code like this.

$('#model').on('change',async function(){
    var modelID = $(this).val();
    var yearsetID = $('#year').val();
    var makesetID = $('#make').val();

    if (!modelID)
        return $('.product').html('Select Some Values');

    try {
        var html = await $.ajax({
            url:'lib/ajaxData.php',
            type: "POST",
            data:{
                "action" : 'getsubmodels',
                "makeset_id" : makesetID,
                "yearset_id" : yearsetID,
                "model_id" : modelID
            },
            dataType: "html"
        }) //If the ajax fail with 404, 500, or anything error, will be catched below...

        if (!html)
            return console.log('No submodel');

        $('#submodel').html(html);
        $('#submodel').on('change', async function(){
            var submodelID = $(this).val();
            var yearsetID = $('#year').val();
            var makesetID = $('#make').val();
            var modelsetID = $('#model').val();

            try {
                var html2 = await $.ajax({
                    type:'POST',
                    url:'lib/ajaxData.php',
                    data: {
                        "action" : 'getbodytypes',
                        "year": yearsetID,
                        "make" : makesetID,
                        "model" : modelsetID,
                        "submodel" : submodelID
                    }
                })

                if (!html2)
                    return console.log('No submodel 2')

                $('#bodytype').html(html);
                $('#bodytype').on('change',async function(){
                    //more dropdowns
                })
            } catch (e) {
                console.log('Error 2nd ajax', e)
            }
        })
    } catch (e) {
        console.log('Error in 1st ajax', e)
    }   
})
Fernando Carvajal
  • 1,869
  • 20
  • 19