MaterialListValueBox breaks subtly if setReadOnly(false) is called before attaching
Medo42 opened this issue · comments
Seen in GWT Material Design 2.2
Explaining the bug requires a little bit of background that I had to build up for myself during debugging of this issue. First off, here is how everything works when things are going well:
When the UI is put together in the onLoad() method, the listBox is added as child DOM element of the MaterialListValueBox's element. After that, Materialize.material_select() is used on the listBox element to build the required DOM structures. As part of that, the listBox element is wrapped into a div with class .select-wrapper, so that the resulting structure in the document's DOM is .listbox-wrapper [Element of MaterialListValueBox] > .select-wrapper > select [Element of listBox]. If the element has to be rebuilt, Materialize.material_select will first unwrap the element again (so the DOM becomes once more .listbox-wrapper [Element of MaterialListValueBox] > select [Element of listBox]) and then re-wrap it. So far so good.
Now if you call MaterialListValueBox.setReadOnly(false), it will rebuild the materialize select. If you call it before the MaterialListValueBox is attached, then onLoad() has not run yet, and so the DOM element of listBox has no parent yet. In that case, when material_select wraps the element, the .select-wrapper is set as parent of the listBox element, but is not referenced by anything else.
If we attach the MaterialListValueBox after that, onLoad() is called which will add the listBox as a child of MaterialListValueBox's element. Since it's the listBox element directly which is added, its .select-wrapper parent is simply replaced by MaterialListValueBox's element, so that the DOM now looks like .listbox-wrapper [Element of MaterialListValueBox] > select [Element of listBox] - i.e. it is no longer wrapped, despite being initialized.
The onLoad() execution continues in the load() method, then, where material_select is called once more. It will detect that the element was already initialized and needs rebuilding, and will proceed to unwrap it. However, the parent that gets tossed out of the DOM this time is not the expected .select-wrapper, but the .listbox-wrapper! In other words, the MaterialListValueBox is now de facto no longer attached, and the child widgets are attached directly to its parent wiget's element instead.
I don't know which consequences this can have for the GWT application and the function of the MaterialListValueBox, but it certainly looks like a bug to me. In my application, I didn't really notice the problem until I tried to access the DOM children of the MaterialListValueBox through its element for some accessibility fixes, only to discover that the children were unexpectedly missing. I can imagine other consequences related to CSS and event handling if nothing else.
This bug might not have the highest impact in terms of user inconvenience, but since it is a fairly complicated chain of causes, I believe it will stump many who are impacted by it and thus cause a lot of "developer inconvenience".
The following method can be used as a workaround:
public static void setReadOnlyWorkaround(MaterialListValueBox listValueBox, boolean value) {
if (listValueBox.isAttached()) {
listValueBox.setReadOnly(value);
} else {
listValueBox.getReadOnlyMixin().setReadOnly(value);
}
}
You're right, this listbox component has different way of setting the required property other than the other widgets (In short it needs to be destroyed) We need to check if the listbox is attached before destroying it. Will scan other widgets that has a direct call to the internal lib api to be able to check if they are existed. Thanks for the contribution.