Select2 + Ajax in Chrome extension only works the first time -- why?

Got a bit of an unusual situation. I’m using select2 in a Chrome extension. A Chrome extension has access to a JS file which is sandboxed from the internet. But it can get data from the internet by passing messages back and forth with a privileged JS file using some asynchonous message-passing methods that Chrome exposes to extensions.

Here’s how I’ve set up my sandboxed JS file which interacts with the Chrome Extension’s DOM:

  1. In the ajax.transport callback I add a Listener for messages sent from the unsandboxed JS file (port.onMessage.addListener)
  2. When this listener receives a message, it calls $("#customer").select2({data: msg.response["results"]}).trigger('change');
  3. In ajax.transport I also send a message (port.postMessage) with the value of the text typed into S2
  4. This message is sent to the unsandboxed JS file, hits a URL, gets the results, and sends a message that the listener in #1 is listening for.

I’m doing it this way because both the messages sent between the sandboxed and unsandboxed files are async, so we need a way to make sure the data gets passed back and forth.

This works the first time I run it, but subsequent runs don’t work. Any ideas why?

Sandboxed JS file:

$(document).ready(function() {
  $("#customer").select2({
    ajax: {
      delay: 250,
      transport: function(params, success, failure) {
        port.onMessage.addListener(function(msg, sender) {
          $("#customer").select2({data: msg.response["results"]}).trigger('change');
        });

        port.postMessage({q: params.data.q});
      }
    }
  });
});

JSON:

{
   "results":[
      {
         "id":1,
         "text":"John Smith"
      },
      {
         "id":2,
         "text":"Sally Smith"
      },
      {
         "id":3,
         "text": "Chris Jones"
      }
   ]
}

Video:

In this video the initial call with sm finds the first two results. But the subsequent call with ch returns no results:

Feels like I’m missing something obvious. Any ideas?

Thanks in advance!

I wonder if the problem is because you’re creating a new onMessage listener every time the transport callback runs. So the first time you send your query (“sm”) it creates a listener and then posts a message. But the second time you send your query (“ch”), it creates another listener, and then posts a message. My guess is that the response from that second message is being received by the first listener (or maybe both of them)? In any case, I wonder if you could create the onMessage listener outside of the transport callback, and just post the message in the callback. Then the one listener would handle all returned responses.

1 Like

Thanks for the reply @John30013. That made me twitchy too. FWIW I took it out of the transport callback and realized that perhaps this line:

$("#customer").select2({data: msg.response["results"]}).trigger('change');

Was re-instantiating the S2 plugin on the #customer DOM element without the ajax.transport callback.

I’ve changed the sandboxed JS to:

// open port to message with background.js
var port = chrome.runtime.connect({name: "customers"});

// adds listener to handle messages from background.js
// port.onMessage.addListener(success);
port.onMessage.addListener(function(msg, sender) {
  $("#customer").val(null).trigger('change');

  $("#customer").select2({
    minimumInputLength: 2,
    data: msg.response["results"],
    ajax: {
      delay: 250,
      transport: function(params, success, failure) {
        port.postMessage({q: params.data.q});
      }
    }
  });
});


$(document).ready(function() {
  $("#customer").select2({
    minimumInputLength: 2,
    ajax: {
      delay: 250,
      transport: function(params, success, failure) {
        port.postMessage({q: params.data.q});
      }
    }
  });
});

And it now hits the endpoint everytime I type something into the S2 input.

FWIW this doesn’t seem ideal - I would expect that if I set the data attribute it wouldn’t change other S2 behavior (like e.g. the transport callback). But if I don’t re-instantiate it, it’s like S2 only matches against the list loaded on the first run instead of hitting the remote endpoint.

New problem with this approach though is that the select list isn’t updated with results from the new query:

Feels like I’m missing something blindingly obvious here. Really appreciate your help!

Regarding this:

FWIW this doesn’t seem ideal - I would expect that if I set the data attribute it wouldn’t change other S2 behavior (like e.g. the transport callback). But if I don’t re-instantiate it, it’s like S2 only matches against the list loaded on the first run instead of hitting the remote endpoint.

The data attribute and the ajax callback are two different ways to provide data to the S2. It’s not all that surprising to me that setting one might clear the other.

You should be able to return the results of the postMessage call from within your custom transport function. What I think you’re missing is calling the success callback that is passed to your transport function. I believe you should be passing the response from postMessage() to that success callback. Since you need to do that from within your onMessage listener, I’m thinking perhaps if you set up the listener right after the postMessage call (all inside your transport function) and have the listener call the success callback with msg.response, that might do it. I also think you would need to arrange to only add the onMessage listener once—not every time the transport function is called—perhaps by keeping a global flag indicating whether you’ve already set it up. Something like this:

// open port to message with background.js
var port = chrome.runtime.connect({name: "customers"});
var listenerInstalled = false;

$(document).ready(function() {
  $("#customer").select2({
    minimumInputLength: 2,
    ajax: {
      delay: 250,
      transport: function(params, success, failure) {
        if (!listenerInstalled) {
          port.onMessage.addListener(function(msg, sender) {
            // I'm not sure you need to clear the current selection,
            // but I've left that code in for now.
            $("#customer").val(null).trigger('change');
            success(msg.response["results"]);
          }
          listenerInstalled = true;
        }
        port.postMessage({q: params.data.q});
      }
    }
  });
});
1 Like

Hey @John30013 thanks for your help. Your code worked except this:

success(msg.response["results"]);

had to be changed to this:

success(msg.response);

As the success callback seems to be looking for a results object to pull the array from to populate the dropdown. Many thanks for your help!

1 Like

Great! I’m glad I could help, since it was largely guesswork on my part :slight_smile:.

If you wouldn’t mind, please “like” my response by clicking the :heart: icon under it. It improves my reputation score on this site and lets others know that they can trust my suggestions. I’m giving you a “like” too.

Thanks again!

1 Like

Done and done =)

Appreciate the help!