skip to Main Content

I need to alphabetize the options in a select list. I can’t just switch the innerHTML and value attributes because some options have classes and event listeners attached. Here is what I have so far:

function alphabetizeSelectListItems(theOldList) {
    var theNewList;
    var insertOptionHere;
    theNewList = theOldList.cloneNode(true);
    while(theOldList.options.length > 0) {
        theOldList.options[0].remove();
    }
    while(theNewList.options.length > 0) {
        insertOptionHere = null;
        for(var optionCounter = 0; optionCounter < theOldList.length; optionCounter++) {
            if(theNewList.options[0].innerHTML.toLocaleUpperCase().localeCompare(theOldList.options[optionCounter].innerHTML.toLocaleUpperCase()) < 0) {
                insertOptionHere = optionCounter;
                break;
            }
        }
        if(insertOptionHere == null) {
            theOldList.appendChild(theNewList.options[0]);
        } else {
            theOldList.insertBefore(theNewList.options[0], theOldList.options[insertOptionHere]);
        }
    }
    theNewList.remove();
}

This clones the old list into a temp element, empties the options from the old list, and then copies each option from the temp list back into the old list in its alphabetical place. Then it deletes the temp list.

Problem with this is it copies everything except the event listeners. So I have to use the same select list with the same options but move them around. I’ve looked at bubble sorting examples and tried to adapt them to option elements instead of array elements, but when I swap two elements, I have to store one in a temporary element, and that removes the event listener.

Is this possible? Is there a way to swap around select list options while retaining the event listeners?

2

Answers


  1. Event delegation is a programming paradigm that requires:

    • The element that is registered to an event should be an ancestor of the element intended for interaction (eg. a <button> to "click", an <input> to type into, etc).

    • Write the event handler to conditionally accept only the elements intended for interaction. Exclude the rest by not including them.

    An ancestor element contains children elements.

    The advantages are as follows:

    • Any children elements of the registered ancestor can be delegated to the registered event(s). That also includes any elements added dynamically in the future.

    • There is no limit to the amount of elements that events can be delegated to.

    • These advantages give you almost full control of what does what, how, when, and why.

    Details are commented in the example.

    // Reference <form>
    const ui = document.forms.ui;
    /**
     * Reference all form controls in <form>
     * In this particular layout they are:
     *   - <button>
     *   - <select>
     *   - <output>
     */
    const io = ui.elements;
    // Reference <select>
    const pick = io.pick;
    // Reference <output>
    const logo = io.logo;
    // Array of <option>s
    const opts = Array.from(pick.children);
    
    /**
     * For some reason <select> wouldn't stay in
     * order. I have a workaround:
     *   1. Add [multiple] attribute to <select> tag.
     *   2. Next, after <select> has been referenced
     *      remove the attribute.
     */
    pick.removeAttribute("multiple");
    
    /**
     * This event handler is triggered when <form> is
     * "click"ed. It delegates the "click" event to the
     * button#sort and excludes all other elements.
     * It does this by setting conditions...
     */
    const order = e => {
      /**
       * e.target is the element that the user clicked.
       *  So this handler will do nothing unless the user
       * clicked an element with the #id of "sort".
       */
      if (e.target.id === "sort") {
        // Sort the array of <option>s...
        opts.sort((a, b) => a.value.localeCompare(b.value))
          // Add them back to select#pick...
          .forEach(o => pick.append(o));
        // Adjust <select> and <output> to the first position.
        opts[0].selected = true;
        logo.value = pick.options[pick.selectedIndex].text.slice(0, 2);
      }
    };
    
    /**
     * This event handler just displays a larger icon. 
     * It was added to show that the <select> is safe. 
     */
    const show = e => {
      if (e.target.id === "pick") {
        logo.value = e.target.options[e.target.selectedIndex].text.slice(0, 2);
      }
    };
    
    ui.addEventListener("click", order);
    ui.addEventListener("change", show);
    :root {
      font: 3ch/1.5 "Segoe UI"
    }
    
    form {
      display: flex;
      justify-content: space-evenly;
      align-items: center
    }
    
    select,
    button,
    output {
      display: block;
      padding: 5px;
      font: inherit
    }
    
    #sort {
      padding: 7px 15px;
      cursor: pointer;
    }
    
    #pick {
      min-height: 2.25rem;
      cursor: pointer;
    }
    
    #logo {
      font-size: 3rem
    }
    <form id="ui">
      <button id="sort" type="button">Sort</button>
      <select id="pick" multiple>
        <option value="dragon">🐉 Dragon</option>
        <option value="bull">🐂 Bull</option>
        <option value="ant">🐜 Ant</option>
        <option value="camel">🐫 Camel</option>
      </select>
      <output id="logo">🐉</output>
    </form>
    Login or Signup to reply.
  2. You can have an array of the elements and sort it. Then reintroduce the elements into the parent, using appendChild which will move them without cloning.

    From MDN:
    If the given child is a reference to an existing node in the document, appendChild() moves it from its current position to the new position

    function alphabetizeSelectListItems(theOldList) {
      const children = Array.from(theOldList.options);
      children.sort(function(a, b) {
        return a.innerText.toLocaleUpperCase().localeCompare(b.innerText.toLocaleUpperCase())
      })
      
      children.forEach(child => theOldList.appendChild(child));
    }
    
    alphabetizeSelectListItems(select)
    <select id="select">
      <option>Alpha</option>
      <option>Gamma</option>
      <option>Epsilon</option>
      <option>Beta</option>
    </select>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search