Mithril.js: Stale Select value in Firefox

Created on 1 Mar 2020  ·  10Comments  ·  Source: MithrilJS/mithril.js

In some cases, <select> element rendered by Mithril doesn't display the option provided in value attribute when operating a dynamic list (see Context section for details). This happens specifically in Firefox, while Chrome doesn't have such an issue.

Mithril version: 2.0.4

Browser and OS: Firefox 72.0.1, 64-bit Linux (also tried on 69 LTS with the same result).

Code

The code I tried originally looked essentially like this:

let Panel = {
  view: () => m('.panel',
    /* controls here */
    m('.row',
      m('button', {onclick: () => addItem(items.indexOf( selectedItem() ))}, "+"),
      m('select', {value: state.selected, onchange () {state.selected = this.value}},
        items.map(it => m('option', {key: it.id, value: it.id}, it.title))),
      m('button', {onclick: () => removeItem( selectedItem() )}, "-")),
    /* item details here (overview if nothing selected) */),
};
let addItem = idx => {
  let item = Object.assign({}, items[idx], {title: "?", id: newId()});
  items.splice(idx+1, 0, item);
  state.selected = item.id;
};
let removeItem = item => {
  if (confirm(`Remove item "${item.title}"?`)) {
    items.splice(items.indexOf(item), 1);
    state.selected = null;
  }
};

Steps to Reproduce

  1. Select an item, click +
  2. Click -, then "Ok"

Expected Behavior

  • After step 1, the newly selected item should be selected in dropdown (selected title will be ?)
  • After step 2, the dropdown box should be empty as nothing is selected

Both of these happen in Chrome.

Current Behavior

  • After step 1, the title of the first item is displayed
  • After step 2, the title of the next item after the removed one is displayed

Both of these happen in Firefox.

Context

When the Select element is used to navigate a dynamic list, it's a common practice to automatically have the newly added option selected; and after the currently selected option is removed, one possible action is to unset the selection altogether (depending on UX logic). After I tried to do this, I encountered an issue in Firefox (which doesn't happen in Chrome): when switching to the newly added option value, the one actually displayed was the first in options list; and when unsetting the value after removing the selected one, it instead displayed the option which had followed the one I removed. Calling m.redraw() immediately afterwards from REPL (or triggering a redraw in-app) caused the element to re-render correctly.

After some fiddling, I managed to figure out that Firefox applies special logic to cases when the dropdown options list doesn't have the provided value at the time of setting (and likely similar shenanigans for cases when the selected option is removed that ignore unsetting the value). This led me to producing a functioning workaround:

let Dropdown = {
  options: [],
  onupdate ({attrs: {options, value}}) {
    if (!this.options.some(it => it.id === value) || !options.some(it => it.id === this.value))
      m.redraw();
    Object.assign(this, {options, value});
  },
  view: ({attrs: {options, value, onchange}}) =>
    m('select', {value, onchange},
      options.map(it => m('option', {key: it.id, value: it.id}, it.title)),
};
/* then using new component in place of original 'select' */
Bug

Most helpful comment

@LeXofLeviafan

Yup, this is a bug, but it's more about browser implementations of select, which we all know to be immensely problematic. Here's proof. Works like a charm! There are good reasons why everyone's implementing their own select widgets.

Your best workaround is this:

onupdate: v => { requestAnimationFrame(m.redraw) }

In truth, I'd recommend against fixing this. I'd also recommend _not_ being rude to people who are trying to help you 🤷‍♂

All 10 comments

@LeXofLeviafan Would this work for you? I don't think this is a Mithril issue, but that the selected attribute of the correct option needs to be set manually.

@osban …I have no clue how the hell it happened, but the selected thing (which was the first thing I tried when I encountered this issue) now works completely different from how it did a couple days ago in the very same browsers (I've spent several hours trying to make it work back then) 0_o

Now the new-id thing works just fine, both in your example and my code (with selected hack, that is). The unset-id thing, however, doesn't (and now it doesn't work in Chrome as well…). Here, I've changed up a few things (bringing it closer to my usecase and fixing stuff like using captions as option IDs); you may notice that if you comment out the onupdate and remove an item, the unset won't be applied until the next redraw (which can be triggered by clicking on the header).

@LeXofLeviafan The problem is that selectedIndex will be -1 when choice === '', and also that after removing an item choice will be '' again, so it doesn't correspond with the first item. So you'll need to do something like this. Does that work as intended?

@osban
Read my comment again

@LeXofLeviafan OK, what I misunderstood was that you actually wanted to have the selected value to be empty after a removal. That problem can be solved by manually setting the selectedIndex to -1.
The other issue is what I mentioned (but perhaps not explained clearly enough) about the selectedIndex() function returning -1 when the select value is empty. When the '-' button is clicked in that situation, the last entry is removed. I doubt that is what you intended, which is why I added the if (selectedIndex() > -1) {...} to rem. You can see the total code here.

@osban So… you're suggesting replacing a workaround with a workaround, then?
And as I said, it's a refresh bug – the value _is_ empty, but it's not propagated to DOM/UI until the next redraw.

As for the 'other issue', it's a matter of app logic (ensuring that an item is selected before removing it; the natural way is to check if choice is empty/nil) and has nothing to do with Mithril itself.

@LeXofLeviafan The value is _not_ empty, because the selectedIndex is reset to 0 after the selected element has been removed, and this in turn sets the value to the title of the first list item (see here). So I guess things are being updated, just not how you want them to be updated. A redraw is therefore not needed, just point the select to the right element. To be clear: you can of course use any workaround you prefer, but I think this is a limitation of the select element, rather than a fault in Mithril's redraw. And yes, I could be horribly wrong.

@osban Again, _pay attention when reading_. The value _is_ empty, but it's not _propagated_ to DOM/UI on the _first_ render… Even though doing a redraw _without_ changing any data (which means generating the same VDOM) _will_ propagate it; thus, the issue is that it's not rendered as defined in the first run – and at the very least, the same VDOM is supposed to produce the same DOM, by definition.

@LeXofLeviafan

Yup, this is a bug, but it's more about browser implementations of select, which we all know to be immensely problematic. Here's proof. Works like a charm! There are good reasons why everyone's implementing their own select widgets.

Your best workaround is this:

onupdate: v => { requestAnimationFrame(m.redraw) }

In truth, I'd recommend against fixing this. I'd also recommend _not_ being rude to people who are trying to help you 🤷‍♂

@CreaturesInUnitards thanks for clearing that up!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omenking picture omenking  ·  3Comments

raykyri picture raykyri  ·  4Comments

StephanHoyer picture StephanHoyer  ·  4Comments

mke21 picture mke21  ·  3Comments

pygy picture pygy  ·  4Comments