0

I have a select list. Each option is a bundle of short texts. When a specific bundle is selected, the texts are displayed in two tables. Each row has a "Delete" icon, so that a text can be removed from the bundle. I want both the tables and the select list to refresh after deletion. There are three chained calls:

  1. delete text from db >>

  2. refresh select list and dump some data in a custom tag >>

  3. get data from custom tag and rebuild tables

But they seem to be firing in the order 3 >> 1 >> 2. The solution I am trying to reproduce is this. What am I doing wrong?

Thank you for your help!

~~~~~~~~~~~~~

UPDATE

1 and 2 are definitely executing sequentially (see screenshot). The problem is with 3 ('createSegTableRows'). This is the one that does NOT make a call to the server.

~~~~~~~~~~~~~

Here's the code. (The broken bit is the last block in the first snippet.)

snippet 1: This fires when an option is selected from the select list:

// This fires when a bundle is selected.
function selectBundle() {

  createSegTableRows();
  rowIconActions();

  // this creates some rows for a couple of tables
  function createSegTableRows() {

    // get "value" from the currently selected option
    var bundle_id = $('select#bundleSelector').find(':selected').prop('value');

    // get some strigified JSON data stored in a custom tag
    var inTagData = $('select#bundleSelector').find(':selected').attr('data');
    // convert back to a JSON object
    var inTagData_parsed = JSON.parse(inTagData);

    // convert some data from inside the JSON object to HTML table rows 
    var st_table_contents = tableRows(inTagData_parsed.st);
    var tt_table_contents = tableRows(inTagData_parsed.tt);

    // populate the tables
    $('#st_seg_table').html(st_table_contents);
    $('#tt_seg_table').html(tt_table_contents);

    // this converts JSON data into table rows
    function tableRows(rObj) {

      // map to array and sort
      var rArray = $.map(rObj, function(el) {
        return el;
      });
      rArray.sort(function(a, b) {
        return a.join_id > b.join_id;
      });

      // create rows 
      var rows = ""
      for (i = 0; i < rArray.length; i++) {
        var segment_id = rArray[i]['segment_id'];
        var join_id = rArray[i]['join_id'];
        var segment_text = rArray[i]['seg_text'];

        // each row has some text and Up/Down/Delete buttons
        rows += "<tr><td class='sid tt'>" + segment_id + " <a title='Up' jid='" + join_id + "'>&#9650</a><a title='Down' jid='" + join_id + "'>&#9660</a> <a title='Remove' jid='" + join_id + "'>&#10005</a> </td><td>" + segment_text + "</td></tr>";
      }

      return rows;
    }
    console.log("some table rows");
  }

  // actions fired by Up/Down/Delete in each row
  function rowIconActions() {

    // find selected option in a <select> list
    var bundle_id = $('select#bundleSelector').find(':selected').prop('value');

    // attach an action to the Delete buttons in each table row 
    $('td.sid>a[title="Remove"]').click(function() {

      var join_id = $(this).attr('jid');
      var role = $(this).parent().prop('className').split(" ")[1];

      // THIS IS THE BIT THAT DOESN'T WORK
      if (join_id && bundle_id) {
        $.post(
          // delete record in db
          'ajax/bundle_delete_join.php', {
            bid: bundle_id,
            jid: join_id
            // rebuild <select> list
          }).then(function() {
          return bundleSelector();
          console.log("some stuff");
          // rebuild tables
        }).done(function() {
          createSegTableRows();
          console.log("done");
        });
      }
    });
  }
}

snippet 2: This repopulates the select list:

// This repopulates the select list.
function bundleSelector() {

  if ($("#pairButton").text("Unpair")) {
    var pid = $("#pairButton").attr("pairid");
  }

  $.post(
    // collect some criteria and retrieve stuff from db
    'ajax/bundle_selector.php', {
      st: $("#txtId_left").html(),
      tt: $("#txtId_right").html(),
      pair: pid,
      filter_st: $('#bundleFilterCheck_st').prop('checked'),
      filter_tt: $('#bundleFilterCheck_tt').prop('checked'),
      filter_pair: $('#bundleFilterCheck_pair').prop('checked')
    },
    function(data) {

      if (data) {

        // convert results to a JSON object
        var dataObj = JSON.parse(data);

        // create a variable for the options
        var options = '';

        if (dataObj != "") {

          // loop through the JSON object...
          Object.keys(dataObj).forEach(key => {

            var bundle_id = key;
            var role = dataObj[key];

            options = options + "<option value='" + bundle_id + "' data='" + JSON.stringify(role) + "'>bundle " + key;

            // loop some more...
            Object.keys(role).forEach(key => {

              if (role[key] && key != 'comment' && JSON.stringify(role[key]) != '[]') {

                options = options + " | " + key + ":";

                var segment_id = role[key];

                // convert to an array for sorting
                var joinDataArray = $.map(segment_id, function(el) {
                  return el;
                });

                // sort the array
                joinDataArray.sort(function(a, b) {
                  return a.join_id > b.join_id;
                });

                // loop through the array
                for (i = 0; i < joinDataArray.length; i++) {

                  var sid = joinDataArray[i]['segment_id'];

                  options = options + " " + sid;

                }
              }
            });
            // add a closing tag to each option
            options = options + "</option>";
          });
          // populate parent element
          $('select#bundleSelector').html(options);
          console.log("some select options");

        } else {
          // if there are no results...
          $('select#bundleSelector').html("");
          $('table#st_seg_table').html("");
          $('table#tt_seg_table').html("");
          $('textarea#bundle_comment').html("");
        }
      } else {
        // and again
        $('select#bundleSelector').html("");
        $('table#st_seg_table').html("");
        $('table#tt_seg_table').html("");
        $('textarea#bundle_comment').html("");
      }
    }
  );
}
user4212
  • 1
  • 4
  • 1
    Does 'ajax/bundle_delete_join.php' immediately return a response, or does it send the response only after the database record has been deleted? And 'bundleSelector' isn't a promise. So it will get called and returned immediately. I suspect you are making your request to the server, which responds immediately, then 2 gets called and returns immediately, triggering 3, 3 then completes first, then 1 then 2 because 3 is the fastest, then 1 then 2. – James Sep 23 '17 at 16:23
  • 2
    Is there a particular reason that you have a .then() and .done()? – IzzyCooper Sep 23 '17 at 16:46
  • James, it looks like 'bundle_delete_join.php' it is a returning a response after the database update. I don't think the problem is at the php end. There are basically two things to update: a select list and a table. The select list is updated directly from the database, and it is behaving correctly. The table should be getting updated from some data in the html in the select list, and this is where the problem is. It sounds like you are right about the rest. – user4212 Sep 23 '17 at 17:19
  • Izzy, if I'm comparatively new to this (especially to ajax chains). I was under the impression that 'done' effectively closes the chain, but it looks like I was wrong. – user4212 Sep 23 '17 at 17:22

1 Answers1

0

The key here lies in understanding that bundleSelector() is asynchronous.

In "THE BIT THAT DOESN'T WORK" you correctly return bundleSelector(); but that function returns undefined. In order for something to run after bundleSelector() has completed, it must return a promise and, for createSegTableRows() to work as expected, that promise must fulfill not only when its $.post() has returned but also when all the option-building is complete.

To achieve that, bundleSelector()'s $.post(..., fn) call must be modified to return $.post(...).then(fn), otherwise the caller will not wait for the option-building to complete before proceeding to createSegTableRows(); hence the 3-1-2 execution order you report.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Roamer-1888, thank you very much for your reply! That actually makes a lot of sense, but I still can't get it to work. Just modified the insides of bundleSelector() to set some variables; return $.post(url, { stuff... }).then(function(data){ loop through and update html... }); I am getting the same behaviour as before. – user4212 Sep 25 '17 at 12:03
  • Eventually figured out why it wasn't working. It was something really dumb. I forgot to allow for the possibility of bundleSelector() returning no results, so the JSON parser in the next function down the line was stalling after being sent 'undefined'. Thank you again for your help! – user4212 Sep 25 '17 at 14:09
  • I wondered if you would spot that! There's actually at least two possible failure cases in `bundleSelector()` that need to be catered for; 1) no results; 2) `data` is falsy. There's a number of ways to handle those cases, the cleanest of which is to force the promise returned by `bundleSelector()` to settle on its error path. That way, you can call `$.post(...).then(bundleSelector).then(createSegTableRows)` and be sure that `createSegTableRows()` will run only if `bundleSelector()` is successful. – Roamer-1888 Sep 25 '17 at 19:42
  • Thank you. Juts fixed the falsy `data` thing. The `createSegTableRows()` doesn't actually get its data directly from `bundleSelector()`. The `bundleSelector()` dumps the data as a string attached to a custom tag on an HTML element; `createSegTableRows()` collects it from there (as do a bunch of other functions). I've now set it up so that, if `bundleSelector()` can't process the data, it just empties out the custom the tag, and `createSegTableRows()` duly returns an empty table. – user4212 Sep 27 '17 at 12:19