asyncee/django-easy-select2

Inline relations: "Add another <Model>" breaks dropdown boxes

TomNaessens opened this issue · 15 comments

I have a model which has a relation to another model. This model has some fields, which use the Select2 dropdown singleselect boxes. When using the "Add another " link:
screen shot 2014-08-11 at 12 40 47
A new row is added to the table. However, when trying to select a value, the dropdown box doesn't "open".

This is the HTML of a working dropdown:

<td class="field-action_owner">
    <div class="select2-container" id="s2id_id_membership_set-6-action_owner" style="width: 250px;">
        <a href="javascript:void(0)" onclick="return false;" class="select2-choice" tabindex="-1">
            <span class="select2-chosen">foo@bar.be</span>
            <abbr class="select2-search-choice-close"></abbr>
            <span class="select2-arrow"><b></b></span>
        </a>
        <input class="select2-focusser select2-offscreen" type="text" id="s2id_autogen17">
    </div>
    <select name="membership_set-6-action_owner" id="id_membership_set-6-action_owner" tabindex="-1" class="select2-offscreen">
        <option value="">---------</option>
        <option value="1" selected="selected">foo@bar.be</option>
        <option value="2">foo@bar.com</option>
    </select>
    <script>
        $("#id_membership_set-6-action_owner").on('select2changed', function(e){
            $("#id_membership_set-6-action_owner").select2({"width": "250px"});
        }).trigger('select2changed');
    </script>
    <a href="/admin/auth/user/add/" class="add-another" id="add_id_membership_set-6-action_owner" onclick="return showAddAnotherPopup(this);"> <img src="/django/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another"></a>
</td>

This is the HTML of a faulty dropdownbox:

<td class="field-action_owner">
    <div class="select2-container" id="s2id_id_membership_set-7-action_owner" style="width: 250px;">
        <a href="javascript:void(0)" onclick="return false;" class="select2-choice" tabindex="-1">   
            <span class="select2-chosen">---------</span>
            <abbr class="select2-search-choice-close"></abbr>   
            <span class="select2-arrow"><b></b></span>
        </a>
        <input class="select2-focusser select2-offscreen" type="text" id="s2id_autogen19">
        <div class="select2-drop select2-display-none select2-with-searchbox">
            <div class="select2-search">
                <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="select2-input">   
            </div>
            <ul class="select2-results"></ul>
        </div>
    </div>
    <select name="membership_set-7-action_owner" id="id_membership_set-7-action_owner" tabindex="-1" class="select2-offscreen">
        <option value="" selected="selected">---------</option>
        <option value="1">foo@bar.be</option>
        <option value="2">foo@bar.com</option>
    </select>
    <a href="/admin/auth/user/add/" class="add-another" id="add_id_membership_set-7-action_owner" onclick="return showAddAnotherPopup(this);"> <img src="/django/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another"></a>
</td>

The major difference I see is that the <script> tag is not loaded, which triggers the select2changed function.
In addition, the faulty dropdown also has a select2-drop div underneath the focusser.

Hello, and thank you for detailed description of a problem. I will investigate the problem as soon as possible!

My current findings are the following:

Django already creates a hidden row beneath the table for the "extra" instance. The select2 selects works there perfectly. When the user clicks the "add another " button, the row gets duplicated and the IDs change of the "old" hidden row.

The change of IDs probably breaks the select2 handlers, but I yet have to confirm this.

For now i'm trying to build the sample application to reproduce the problem.

Disabling

        var updateElementIndex = function(el, prefix, ndx) {
            var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
            var replacement = prefix + "-" + ndx;
            if ($(el).attr("for")) {
                $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
            }
            if (el.id) {
                el.id = el.id.replace(id_regex, replacement);
            }
            if (el.name) {
                el.name = el.name.replace(id_regex, replacement);
            }
        };

from https://github.com/django/django/blob/master/django/contrib/admin/static/admin/js/inlines.js renders a second (but working) box next to the faulty box when a new instance is added.

One very barbaric decision may be something like this:

$(addButton).on('click', function(e){
    e.preventDefault();
    $('tr.form-row script').each(function(item){
        eval(item.text);
    })
})

There are three problems with such approach:

  1. Eval is evil
  2. It may be a trouble to find correct tr element
  3. prefix problem in script is not solved
  4. Event order is not defined, so this code may be executed first, and code from inlines.js second.

As a workaround of problem 3 it is possible to replace prefix with input#id_<model_name>_set-TOTAL_FORMS.

Still, this approach looks very bad to me.

Another round of debugging led me to the following (although this may be incorrect) conclusion:

The addbutton finds the (working, hidden) templaterow (https://github.com/django/django/blob/master/django/contrib/admin/static/admin/js/inlines.js#L60). It then clones the row (https://github.com/django/django/blob/master/django/contrib/admin/static/admin/js/inlines.js#L61), leaving the IDs (still prefix at this point) and script intact. A bit further, they update the IDs from prefix to the IDs used in the table: https://github.com/django/django/blob/master/django/contrib/admin/static/admin/js/inlines.js#L78. This function however only replaces the name and id, leaving the <script> intact.

The resulting, inserted row looks like this at the point of insertion: (snipped a bit)

...

<div class="select2-container" id="s2id_id_membership_set-16-action_owner" style="width: 250px;"><a href="javascript:void(0)" onclick="return false;" class="select2-choice" tabindex="-1">   <span class="select2-chosen">---------</span><abbr class="select2-search-choice-close"></abbr>   <span class="select2-arrow"><b></b></span></a><input class="select2-focusser select2-offscreen" type="text" id="s2id_autogen118"><div class="select2-drop select2-display-none select2-with-searchbox">   <div class="select2-search">       <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="select2-input">   </div>   <ul class="select2-results">   </ul></div></div><select name="membership_set-16-action_owner" id="id_membership_set-16-action_owner" class="select2-offscreen" tabindex="-1">
...
       <script>
            $("#id_membership_set-__prefix__-action_owner").on('select2changed', function(e){
                console.log('id_membership_set-__prefix__-action_owner triggered select2changed');
                $("#id_membership_set-__prefix__-action_owner").select2({"width": "250px"});
            }).trigger('select2changed');
        </script>

So yes, the above script may work if we regex the script to contain the correct selectors.

Sorry, but for a few days i will not be able to work on this problem.

It looks like i haven't got enough time to fix it yet. So, contributions highly appreciated!

Hey,

Here how I did it (this is a first draft, so refactoring required) - https://github.com/bashu/django-easy-select2/commit/2c7ed05497723371eeb06cfeace42b4ff382ecdf

result for StackedInline:

screen shot 2014-09-08 at 11 47 28

Thank you, bashu! I'll try to get on this in a few days. In general, everything is clear in your implementation, but deleted 'Select2TextMixin' and 'Select2TextInput'. Why?

I was lazy to update them... I will try to fix this tomorrow

I have adopted your code with a bit of refactoring, also fixing Select2TextMixin.

You can see changes: https://github.com/asyncee/django-easy-select2/tree/issue-12

I did some manual testing and it seems everything working fine.
Although project lacks of automated tests, so can you please test it?

Looks good, I approve ;-) Is there a chance you can release minor update on pypi soon?

Updated package to version 1.2.9. Release uploaded to pypi. Thanks to everybody, especially, bashu!

I just tested this, and it works as expected, thx for this fix @bashu , and good work @silox ;-)