Can't unselect new option created with "Tags" feature

I’ve got a strange problem:

  1. I’ve got an Ajax datasource
  2. I want people to be able to create a new option
  3. It’s all happening 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.

The problem is that if I:

  1. Create a new option in the list e.g. “Foobar” and select it
  2. Create a second option in the list e.g. “Fizzbuzz”

FizzBuzz will never appear in the list. Only Foobar OR a result from the Ajax search will appear. Here’s a video to make the problem more clear:

https://uc1c6fc5b80749549df968d22f8e.dl.dropboxusercontent.com/cd/0/inline/AVXynfaUyH8ZIllmnNyJ2Q488TUxnQN3CgV9sDh0Jh_lQ32yYTzAlEkMCauXqYh0rBxEN7uEP6dsgOIN2kHSELLoQoraI7JyaSdkX5t0R57iFyRGcfio0TikPeD8Xbt1xFdQ5hR3dK1C91ouc7-ZVttyHk4inInDQ6xlNsx4VgOrlmoxEOd_dBVgcRPQP_G_vzg/file

Code:

Popup.js

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

// we haven't installed the listener on the customer port yet
var custListener = false;

$("#customer").select2({
  minimumInputLength: 2,
  allowClear: true,
  cache: true,
  placeholder: "Search for a customer",
  ajax: {
    delay: 500,
    transport: function(params, success, failure) {
      if (!custListener) {
        custPort.onMessage.addListener(function(msg, sender) {

          // this just stuffs the API response into the dropdown.
          success(msg.response);
        });
        // we've now installed the customer listener
        custListener = true;
      }
      custPort.postMessage({q: params.data.q});
    },
  },
  tags: true,
  createTag: function (params) {
    var term = $.trim(params.term);

    if (term === '') {
      return null;
    }
    return {
      id: term,
      text: term,
      newTag: true
    }
  },
  insertTag: function (data, tag) {
    // Insert the tag at the end of the results
    data.push(tag);
  },
  templateResult: function (data) {
    var term = $("#customer").data("select2").dropdown.$search.val();
    var reg = new RegExp(term, 'gi');
    var optionText = data.text;
    var boldTermText = optionText.replace(reg, function(optionText) {return "<strong>" + optionText + "</strong>"});
    var $result = $("<span>" + boldTermText + "</span>");

    if (data.newTag) {
      $result.prepend("<strong>New Customer: <strong> ");
    }

    return $result;
  }
});

Sandboxed JS file:

  chrome.runtime.onConnect.addListener(function(port) {
    port.onMessage.addListener(loadXMLDoc);
  })

  function loadXMLDoc(msg, sender, sendResponse) {
    
    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onload = function() {
      if (xmlhttp.status === 200) {
        port.postMessage({ 
          response: JSON.parse(this.responseText)
        });
      }
    }

    if (port.name == "customers" && msg.q) {
      xmlhttp.open("GET", "http://localhost:8001/app/api/users/?search=" + msg.q, true);

      // Go hit endpoint
      xmlhttp.send();
    }
  }

I’ve tried replicating this behavior outside of a Chrome Extension but everything works as normal. Which is to say that I can create different Tags:

Any idea what I might be missing in the first scenario to get Tags working with Ajax?

Thanks in advance!

The link to the first video gives me a 404 error page. Can you put it somewhere else so I can see what is happening?

Have you tried setting some breakpoints in your code where you expect things to happen that apparently aren’t? If your code is taking all the expected paths then this might be a bug or an incompatibility between Select2 and Chrome extensions.

@John30013 edited video link, sorry about that. Here’s the error case:

https://uc1c6fc5b80749549df968d22f8e.dl.dropboxusercontent.com/cd/0/inline/AVXynfaUyH8ZIllmnNyJ2Q488TUxnQN3CgV9sDh0Jh_lQ32yYTzAlEkMCauXqYh0rBxEN7uEP6dsgOIN2kHSELLoQoraI7JyaSdkX5t0R57iFyRGcfio0TikPeD8Xbt1xFdQ5hR3dK1C91ouc7-ZVttyHk4inInDQ6xlNsx4VgOrlmoxEOd_dBVgcRPQP_G_vzg/file

I’ll dig more into the testing this week, but at first blush all seems normal. It’s just that S2 never inserts the correct tag after there’s a tag created. Please LMK if you see anything that’s obviously off. And I’ll report back with any findings. Thanks!

Hey, kareem–

What happens if you click the “x” in the S2 after having selected “foobar”? Are you then able to create a new entry?

Hi @John30013,

If I click the ‘x’ I get the same behavior when I create a new entry - the same option is selected.

I’ve added some logging in my createTag method:

      createTag: function (params) {
        console.log("Creating Tag");
        console.log("params");
        console.log(params);
        var term = $.trim(params.term);

        console.log("term");
        console.log(term);

        if (term === '') {
          return null;
        }
        return {
          id: term,
          text: term,
          newTag: true
        }
      },

Here’s a video showing the logged output and clicking “x”. It seems like the parameter that createTag takes (which is called params in this case) is never updated to the new value that gets typed into the dropdown. In the second case params.term should be “Fizz Buzz”, not “Foo Bar”. Hmmm…

More digging with breakpoints. Looks like the second time through select2 the value of params is not updated to the value it should have the second time through. I’m out of my depth as to how to fix but can definitely take guidance and run with it if you have any perspectives. Thank!

I was going to ask if you could post your code on Codepen or JSFiddle, but I think you said this works fine in a regular browser window, and that it only fails in the context of a Chrome extension.

Is it possible for me to get a copy of your extension (and would it work for me)? It’s very hard to try to diagnose with just the videos to go on and without being able to see all of the code.

It’s entirely possible that there is some fundamental incompatibility between Chrome extensions and Select2. But it seems odd that it works the first time you type into it, but not any time after that.

I have a theory (although I don’t know how to resolve it if it’s true) that this is related to Select2 reusing the param object rather than creating a new one each time. I’ve used other frameworks that track things based on objects, and if an object is just reused (i.e., its properties are updated but the object reference stays the same) rather than recreated, the framework cannot detect that the object has changed and so the framework’s internal state remains unchanged. But if something like that is happening here I’m not sure what you could do about it, because I don’t think you have control over the creation of the param object. (I suppose it might be possible for you to find some event hook [maybe in a custom matcher] where you could clone the param object into a new object which you then assign to the param variable.) But this is just speculation on my part and, without being able to dig more deeply into your code, I really can’t do much more than just guess.

1 Like

Hey John,

Your theory aligned with mine. Let me dig a little more on my end. Will update the thread once I get as far as I can. Appreciate your offer to help - may take you up on it but don’t want to waste your time =)

@John30013 Figured this out. The issue was that the value of params was set before the anonymous function defined in the custPort message listener. I think the anonymous function is a closure, so its value of params won’t be changed on subsequent S2 searches.

We changed this:

  ajax: {
    delay: 500,
    transport: function(params, success, failure) {
      if (!custListener) {
        custPort.onMessage.addListener(function(msg, sender) {

          // this just stuffs the API response into the dropdown.
          success(msg.response);
        });
        // we've now installed the customer listener
        custListener = true;
      }
      custPort.postMessage({q: params.data.q});
    },
  },
  tags: true
}

To this:

  ajax: {
    delay: 500,
    transport: function(params, success, failure) {
      function handleResponse(msg, sender) {
        success(msg.response);
      }
      // Remove the old listener with the old value of params
      custPort.onMessage.removeListener(handleResponse);

      // Add a new listener with the current value of params
      custPort.onMessage.addListener(handleResponse);
      custPort.postMessage({q: params.data.q});
    },
  },
  tags: true
}

Since the old listener is torn down before the new one is created, the new one is thus created with the current value of params (e.g. what’s most recently been typed into the search box).

Thanks for your help!

1 Like

Hi, Kareem–

It’s true that the anonymous listener function is a closure, but since it doesn’t reference the params object*, I don’t think that matters (since the “preserved” value of params would only apply within the anonymous function’s scope).

*I might be misunderstanding the purpose of the listener. Does itsmsg parameter contain the value of params (i.e., is msg equal to {q: params.data.q})? If so, then your explanation is correct.

In any case, I’m glad you found the solution.

Hey John,

Interesting. The msg param does not reference the params object. But for some reason tearing down the listener and re-adding it fixes the problem. Your explanation makes sense, but leaves me baffled.

Can you think of a reason why tearing down the listener would fix the problem? I’d love to bolster my understanding of why this occurred to make it less painful the next time this kind of situation happens.

Kareem

Hey, Kareem–

I can’t think of a good reason why your fix works. I think you mentioned that your previous code worked correctly outside of the Chrome extension (i.e., when running in a regular browser window). So my guess is that it has something to do with the sandbox environment in which Chrome executes extensions. Maybe that environment creates some kind of closure of its own, or maybe there’s something else going on. Honestly I have no clue.

–John