Strange behavior of select/dropdown's onchange() JS event when using 'Next' on Mobile Safari Dropdown list item select box

26,633

I had the same problem on my site. I was able to fix it by manually polling the selectedIndex property on the select control. That way it fires as soon as you "check" the item in the list. Here's a jQuery plugin I wrote to do this:

$.fn.quickChange = function(handler) {
    return this.each(function() {
        var self = this;
        self.qcindex = self.selectedIndex;
        var interval;
        function handleChange() {
            if (self.selectedIndex != self.qcindex) {
                self.qcindex = self.selectedIndex;
                handler.apply(self);
            }
        }
        $(self).focus(function() {
            interval = setInterval(handleChange, 100);
        }).blur(function() { window.clearInterval(interval); })
        .change(handleChange); //also wire the change event in case the interval technique isn't supported (chrome on android)
    });
};

You use it just like you would use the "change" event. For instance:

$("#mySelect1").quickChange(function() { 
    var currVal = $(this).val();
    //populate mySelect2
});

Edit: Android does not focus the select when you tap it to select a new value, but it also does not have the same problem that iphone does. So fix it by also wiring the old change event.

Share:
26,633
Vaibhav Garg
Author by

Vaibhav Garg

.Net Dev

Updated on October 15, 2020

Comments

  • Vaibhav Garg
    Vaibhav Garg over 3 years

    This is a hard one to articulate and I am new to mobile web development so please bear with me:

    On my webpage, I have 3 Nested dropdown lists (Area, Town, Street).

    Nested as in, each dropdown's items are modified when the selection in the dropdown above it changes. e.g selecting an Area changes the Town and Street lists and selecting a Town changes the Street list.

    I use XMLHTTPRequests in the onchange() javascript event of the dropdowns to fetch and populate the other downdowns. This works fine on Android and Desktop browsers.

    On Mobile Safari, when a drowdown is touched, a list is shown where the user can select items. In addition the selection box has the "Previous/Next/Autofill/Done" buttons to navigate to other form elements.

    So the user touches the first dropdown, selects a value and presses the Next button. This causes two problems:

    First, On this action the first dropdown's oncange() event is not triggered reliably! Sometimes it fires sometimes not.

    If after selecting an Area, the user touches somewhere else on the webpage or presses the "Done" button then the onchange() is fired normally and the Towns and Streets are populated normally.

    Second, the element that comes into focus when pressing then "Next" button is the dropdown whos elements need to be changed after being fetched. When the onchange() of the previous dropdown does get fired then, either the list is no updated or the items in the select box turn blue and all of them have a tick mark showing that they are all selected..

    From what i can tell the problem would be solved if i can disable the Next/Previous buttons in the selection box or somehow fix how the onchange() is fired and the next in focus dropdown's list items are repopulated while it is in focus.

    Here is the code (simplified):

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no" />
    
        <title></title>
    </head>
    <body onload="AppStart();">
        <form action="#">
        Area:
        <select id="ddlArea">
            <option value="">-- Select Area -- </option>
            <option value="1">Area 1</option>
            <option value="2">Area 2</option>
            <option value="3">Area 3</option>
            <option value="4">Area 4</option>
            <option value="5">Area 5</option>
        </select>
        <br />
        Town:
        <select id="ddlTown">
            <option value="">Please wait ...</option>
        </select>
        <br />
        Street:
        <select id="ddlStreet">
            <option value="">-- Select Area or Town -- </option>
        </select>
        <br />
        Unit:
        <select id="ddlUnit">
            <option value="">-- No Street Selected -- </option>
        </select>
    
        <script type="text/javascript">
    
            var ddlArea, ddlTown, ddlStreet, ddlUnit;
            function AppStart() {
                ddlArea = document.getElementById("ddlArea");
                ddlTown = document.getElementById("ddlTown");
                ddlStreet = document.getElementById("ddlStreet");
                ddlUnit = document.getElementById("ddlUnit");
    
                ddlArea.onchange = areaChanged;
                ddlTown.onchange = townChanged;
                ddlStreet.onchange = streetChanged;
    
                setTimeout(function() { updateTown(""); }, 250);
            }
    
            var areaId = "", townId = "", streetId = "", unitId = "";
            function areaChanged(e) {
                areaId = ddlArea.options[ddlArea.selectedIndex].value
                ddlClear(ddlTown, createOption("Please Wait...", ""));
                ddlClear(ddlStreet, createOption("Please Wait...", ""));
                ddlClear(ddlUnit, createOption("-- No Street Selected --", ""));
                setTimeout(function() { updateTown(areaId); }, 500);
                setTimeout(function() { updateStreet(areaId, ""); }, 700);
            }
    
            function townChanged(e) {
                townId = ddlTown.options[ddlTown.selectedIndex].value
                ddlClear(ddlStreet, createOption("Please Wait...", ""));
                ddlClear(ddlUnit, createOption("-- No Street Selected --", ""));
                setTimeout(function() { updateStreet(areaId, townId); }, 400);
            }
    
            function streetChanged(e) {
                streetId = ddlStreet.options[ddlStreet.selectedIndex].value
                ddlClear(ddlUnit, createOption("Please Wait...", ""));
                setTimeout(function() { updateUnit(streetId); }, 600);
            }
    
            function updateTown(areaID) {
                ddlClear(ddlTown, createOption("-- Select Town --", ""));
                var items = isNaN(parseInt(areaID)) ? 10 : parseInt(areaID);
                if (areaID == "") areaID = "ALL";
                for (var i = 0; i < items; i++) {
                    ddlTown.appendChild(createOption("Town " + (i+1) + ", Area " + areaID, i));
                }
            }
    
            function updateStreet(areaID, townID) {
                ddlClear(ddlStreet, createOption("-- Select Street --", ""));
                var items1 = isNaN(parseInt(areaID)) ? 10 : parseInt(areaID);
                var items2 = isNaN(parseInt(townID)) ? 10 : parseInt(townID);
                var items = items1 + items2;
                if (areaID == "") areaID = "ALL";
                if (townID = "") townId = "ALL";
                for (var i = 0; i < items; i++) {
                    ddlStreet.appendChild(createOption("Street " + (i + 1) + ", Area " + areaID + ", Town " + townID, i));
                }
            }
    
            function updateUnit(streetID) {
                ddlClear(ddlUnit, createOption("-- Select Unit --", ""));
                var items = isNaN(parseInt(streetID)) ? 10 : parseInt(streetID);
                if (streetID == "") streetID = "ALL";
                for (var i = 0; i < items; i++) {
                    ddlUnit.appendChild(createOption("Unit " + (i + 1) + ", Street " + streetID, i));
                }
            }
    
            function ddlClear(Dropdown, option) {
                while (Dropdown.options.length > 0) {
                    try { Dropdown.options[0] = null; } catch (e) { };
                }
                if (option != null) {
                    Dropdown.appendChild(option);
                }
            }
    
            function createOption(text, value) {
                var oOption = document.createElement("OPTION");
                oOption.innerHTML = text;
                oOption.value = value;
                return oOption;
            }
    
        </script>
    
        </form>
    </body>
    </html>
    

    Help. :/

  • Vaibhav Garg
    Vaibhav Garg over 12 years
    Thanks for this.. I had solved this problem by calling blur() on the next dropdown, when the first one's selection was changed and XMLHttpRequest data was returned, so that the selection box disappeared and the user had to touch the second dropdown to access it, which cause the items to be refreshed in the list.. it was a hack which i dont like much..
  • iJames
    iJames over 12 years
    This is a great solution which did solve the first order failure of the Safari Mobile Web Browser (which is still present on iOS 5). But it seems to have left another secondary effect. Now the "spinner" is populating, but upon hitting "next" or "done" the actual pulldown doesn't seem to update with the selection and doesn't register. Any insights? Also the fix seemed to temporarily break the Android's version of the interface. But then it worked.
  • sancaryum group
    sancaryum group over 12 years
    I discovered the android problem after I posted this. See my edit and new code above for the fix. Can you create a jsfiddle for the first problem?
  • iJames
    iJames over 12 years
    Ah, yes that was my next step! Good show! I'll recreate with JSFiddle. Love that mobile debug solution...
  • wdm
    wdm over 12 years
    +1 for a good work-around. Posted my results, based on my own past question - stackoverflow.com/questions/7680992/…
  • MUG4N
    MUG4N over 11 years
    +1 for saving my f*** day. I searched 6 hours for a solution to this problem. Thx buddy...
  • Behrang
    Behrang about 11 years
    Just for the record, this breaks Firefox due to this long-standing bug: bugzilla.mozilla.org/show_bug.cgi?id=393494
  • Behrang
    Behrang about 11 years
    Furthermore, if you happen to end up using this hack, make sure you reset the value of qcindex if and when you repopulate/manipulate the <option> elements inside the <select> element.
  • Q-Ball
    Q-Ball over 10 years
    Thank you for providing this example. It saved me a lot of time. Although now, with iOS 7 update, this solution is no longer working. I'm trying to debug your code, but so far haven't had any luck getting it to work on the updated iOS. Anyone have any luck?
  • Zain Shaikh
    Zain Shaikh over 10 years
    Any help for iOS7 issue?
  • Progrower
    Progrower over 10 years
    I was wondering the same thing about IOS7. I used another method but I had to remove it because it messed up my form validation.
  • sancaryum group
    sancaryum group over 10 years
    I recently tested IOS7 with this and it still seemed to work for me. I'll have to check again.
  • FlorianB
    FlorianB almost 8 years
    What does jQuery have to do in this? There is no reason to make it big and dirty with extra libraries. All it takes is a timeout function that wraps everything in the onchange.