Closed mturoci closed 1 year ago
I couldn't find a solution yet, but I found out why dropdown doesn't hold its value when submit is clicked.
@mturoci gave me some leads. form
card and bond
function might have something to do with it. After much examination, I didn't find anything there. I removed bond
, turned form
into a class component, and tinkered with componentDidUpdate
, but the problem persisted.
Instead of checking why the dropdown was changing, I decided to investigate why the Slider
component was not. Turns out that it was, but Fluent Slider
's internal logic handles updates differently, not changing its value back to the default when props
change. Also, contrarily to what was previously assumed, components are not being recreated when submit is clicked.
Based on that, I had to look into how Fluent's Dropdown logic works and the snippet bellow can help us understand what is going on.
// fluentui/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.base.tsx
public UNSAFE_componentWillReceiveProps(newProps: IDropdownProps): void {
// In controlled component usage where selectedKey is provided, update the selectedIndex
// state if the key or options change.
let selectedKeyProp: 'defaultSelectedKeys' | 'selectedKeys' | 'defaultSelectedKey' | 'selectedKey';
// this does a shallow compare (assumes options are pure), for the purposes of determining whether
// defaultSelectedKey/defaultSelectedKeys are respected.
const didOptionsChange = newProps.options !== this.props.options;
if (newProps.multiSelect) {
if (didOptionsChange && newProps.defaultSelectedKeys !== undefined) {
selectedKeyProp = 'defaultSelectedKeys';
} else {
selectedKeyProp = 'selectedKeys';
}
} else {
if (didOptionsChange && newProps.defaultSelectedKey !== undefined) {
selectedKeyProp = 'defaultSelectedKey';
} else {
selectedKeyProp = 'selectedKey';
}
}
if (
newProps[selectedKeyProp] !== undefined &&
(newProps[selectedKeyProp] !== this.props[selectedKeyProp] || didOptionsChange)
) {
this.setState({
selectedIndices: this._getSelectedIndexes(newProps.options, newProps[selectedKeyProp]),
});
}
if (
newProps.options !== this.props.options // preexisting code assumes purity of the options...
) {
this._sizePosCache.updateOptions(newProps.options);
}
}
Pay attention to const didOptionsChange = newProps.options !== this.props.options
. options
in this case refers to the ui.dropdown
choices
arguments in our Wave app.
this.props.options
: initial choices
argument
newProps.options
: choices
argument provided when submit is clicked
Using @mturoci's repro code I notice that the didOptionChanges
is always true
. The main issue is the shallow comparison.
Even though the choices
arguments in the Wave app have the same values, they are created in different places, holding different references, so when we compare them with !==
(reference inequality) the result is true
.
As you can see in the if
inside the outermost else
block, when didOptionChanges
is true, selectedKeyProp
is set to 'defaultSelectedKey'
which will be used to fetch the value for the input box in the following if
block.
I don't know how the references are being kept for objects and arrays between the Wave app and React, but I imagine it's a good place to explore possible root causes. @lo5 and @mturoci maybe can give me a hand with this one.
I'm currently looking into other components to check if they have the same issue. If so, we'd have to come up with a more generic approach that solves the problem on a higher level in the component tree.
value
from Wave App:choices
can't be set - #2104 brings support for setting the choices dynamically⚠️ Behavior for setting choice-based components must be unified as well.
We also have value
for some stat cards but the value that is more likely to be updated is data
and we already have many examples for that, proving that they work.
To be tested against master
.
from h2o_wave import main, app, Q, ui
choices = [
ui.choice("A", "Choice A"),
ui.choice("B", "Choice B"),
ui.choice("C", "Choice C"),
]
tabs = [
ui.tab(name="email", label="Mail", icon="Mail"),
ui.tab(name="events", label="Events", icon="Calendar"),
ui.tab(name="spam", label="Spam"),
]
@app("/demo")
async def serve(q: Q):
if q.args.reset:
q.client.initialized = False
if not q.client.initialized:
q.page["controls"] = ui.form_card(box="1 1 2 1", items=[
ui.buttons(items=[
ui.button(name="change", label="Change", primary=True),
ui.button(name="reset", label="reset", primary=True), ]
)
])
q.page["tab"] = ui.tab_card(
box="1 2 4 1",
items=[ui.tab(name="#menu/a", label="A"), ui.tab(name="#menu/b", label="B"), ui.tab(name="#menu/c", label="C"), ui.tab(name="#borken", label="(broken)")],
value="#menu/b",
)
q.page["nav"] = ui.nav_card(
box="1 3 2 -1",
value="#menu/b",
items=[
ui.nav_group("Menu (working)", items=[
ui.nav_item(name="#menu/a", label="A"),
ui.nav_item(name="#menu/b", label="B"),
ui.nav_item(name="#menu/c", label="C"),
])
],
)
q.page["broken"] = ui.form_card(box="3 1 4 -1", items=[
ui.text_xl(name="broken", content="BROKEN"),
ui.checkbox(name="checkbox", label="Checkbox", value=False),
ui.date_picker(name="date", label="Date Picker", value="2017-10-19"),
ui.toggle(name="toggle", label="Toggle"),
ui.spinbox(name="spinbox", label="Spinbox", min=0, max=100, step=10, value=30),
ui.range_slider(name="range_slider_step", label="Range Slider", min=0, max=1000, step=100, min_value=0, max_value=300),
ui.slider(name="slider", label="Slider", min=0, max=100, step=10, value=30),
ui.time_picker(name="timepicker_disabled", label="Time Picker", value="11:15"),
ui.copyable_text(label="Copyable text", value="foo"),
ui.choice_group(name="choice_group", label="Choice Group (can set choices, does not clear selection)", value="B", choices=choices),
ui.picker(name="picker", label="Picker (can set choices, does not clear selection)", choices=choices, values=["A"]),
])
q.page["working"] = ui.form_card(box="7 1 4 -1", items=[
ui.text_xl(name="broken", content="WORKING"),
ui.progress(label="Progress", value=0.5),
ui.tabs(name="menu", value="spam", items=tabs),
ui.textbox(name="textbox", label="TextBox", value="foo"),
ui.combobox(name="combobox", label="Combobox single (can set choices, clears selection)", value="a", choices=["a", "b", "c"]),
ui.combobox(name="combobox2", label="Combobox multi (can set choices, clears selection)", values=["a"], choices=["a", "b"]),
ui.checklist(name="checklist", values=["A", "B"], label="Checklist (can set choices, does not clear selection)", choices=choices),
ui.color_picker(name="color", label="Color picker (can set choices, does not clear selection)", value="yellow", choices=["yellow", "green"])
])
q.client.initialized = True
if q.args.change:
q.page["broken"].items[1].checkbox.value = True
q.page["broken"].items[2].date_picker.value = "2022-03-22"
q.page["broken"].items[3].toggle.value = True
q.page["broken"].items[4].spinbox.value = 50
q.page["broken"].items[5].range_slider.min_value = 50
q.page["broken"].items[5].range_slider.max_value = 60
q.page["broken"].items[6].slider.value = 50
q.page["broken"].items[7].time_picker.value = "13:00"
q.page["broken"].items[8].copyable_text.value = "bar"
q.page["broken"].items[9].choice_group.value = "D"
q.page["broken"].items[9].choice_group.choices = [ui.choice("B", "Choice B"), ui.choice("C", "Choice C"), ui.choice("D", "Choice D")] # can be set but does not clear the selection
q.page["broken"].items[10].picker.values = ["B"]
q.page["broken"].items[10].picker.choices = [ui.choice("B", "B")] # can be set but does not clear the selection
q.page["tab"].value = "#menu/c"
q.page["working"].items[1].progress.value = 0.75
q.page["working"].items[2].tabs.value = "events"
q.page["working"].items[3].textbox.value = "changed"
q.page["nav"].value = "#menu/c"
# To test dropdown use https://github.com/h2oai/wave/pull/1621#issue-1372662766
q.page["working"].items[4].combobox.value = "b"
q.page["working"].items[5].combobox.choices = ["b", "c"] # can be set and clears the selection
q.page["working"].items[5].combobox.values = ["b"]
q.page["working"].items[5].combobox.choices = ["b", "c"] # can be set and clears the selection
q.page["working"].items[6].checklist.values = ["B"]
q.page["working"].items[6].checklist.choices = [ui.choice("B", "Choice B")]
q.page["working"].items[7].color_picker.value = "#FFFFF8"
q.page["working"].items[7].color_picker.choices = ["yellow", "green", "orange"] # can be set but does not clear the selection
await q.page.save()
@marek-mihok can you please finish this once you are back from vacation? You can either make a single PR or multiple smaller ones, whatever suits you.
via @psinger
Repro
Actual behavior
After changing dropdown value and hitting submit, dropdown value should respect user input, but is currently overwritten by the
value
attribute in the else block (2).Desired behavior
value
should be initial value only and dropdown should then be fully controlled by the user.