lona
lona copied to clipboard
Groups of radioboxes: Not all Python-Objects updated correctly?
I am currently implementing an interface where I want to place a group of radioboxes (<input type='radio' ...>) where an user can select one-of-many.
I've started by copying the Checkbox from lona.html.data_binding_inputs to get the following:
class Radiobox(CheckBox):
INPUT_ATTRIBUTE_NAME = 'checked'
ATTRIBUTES = {
'type': 'radio',
}
def __init__(self, value=False, disabled=False, readonly=False,
bubble_up=False, **kwargs):
super().__init__(value, disabled, readonly, bubble_up, **kwargs)
if 'input_delay' not in kwargs:
self.events = [CHANGE]
@property
def value(self) -> bool:
value = self.attributes.get(self.INPUT_ATTRIBUTE_NAME, False)
if value == '': # is possible after parsing HTML string
return True
return bool(value)
@value.setter
def value(self, new_value: bool) -> None:
if not isinstance(new_value, bool):
raise TypeError('value is a boolean property')
self.attributes[self.INPUT_ATTRIBUTE_NAME] = new_value
Next step was to create a group of radioboxes like this:
Div(
Radiobox(_id=1, name='groupname'),
Radiobox(_id=2, name='groupname', checked=True),
)
When toggling the radiobox in the frontend to the other one I end up with both nodes on the python-side having checked=True.
It seems as the other Radiobox does not emit an input_event to update it's state.
On a high level I would expect both Radoboxes to update at the same time. What is the expected behavior here?
Workaround:
My plan was to have Radiogroup-Widget anyway. So I have a single object that I can query for the current value and that creates the Radioboxes and Labels by itself.
But since I can not rely on the .value of the radio-boxes I am currently tracking the state of the group by hand. And this feels wrong.
Here is my prototype:
class Radiogroup(Widget):
def __init__(self, options, checked=None):
"""
Creates a Group of radiobuttons.
Arguments:
* options: Dict of options
{ 'id-name 1': 'Screen Name 1', ... }
* checked: String with the <id> of the pre-checked item
"""
self._mapping = {}
div = Div()
for ident, screenname in options.items():
rb = Radiobox(_id=ident, name=f'radiogroup-{self.id}', bubble_up=True)
if ident == checked:
rb.attributes['checked'] = 'True'
div.append(rb)
div.append(Label(screenname, _for=ident))
div.append(Br())
self._mapping[rb.id] = ident
self.nodes = [div]
self._value = checked
def handle_input_event(self, input_event):
self._value = self._mapping[input_event.node.id]
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
for node in self.query_selector_all('input'):
if self._mapping[node.id] == new_value:
node.value = True
else:
node.vale = False
direction = Radiogroup(
{
'received': 'Payment received',
'sent': 'Payment sent',
},
checked='received',
)
I would really like to see this Widget as part of Lona at some point. But I would like to clarify my understanding of the event-system before contributing any code :-)
Hi @SmithChart
You are right, Radiobuttons are an unsolved problem in Lona at the moment. I had an idea for a similar widget in the past, but the problem with an backend-side approach like that is that you have to enforce some kind of HTML structure in which your radio buttons live. For example: Your widget uses divs as structural elements. That means (without CSS) your Radiobuttons would be grouped below each other. We could make that configurable with a callback or something like that, but you get the point.
In the browser Radiobuttons work like this: Radiobuttons are like textinputs consisting of an name and a value you can't change. For every name you can have multiple values (that's how you make them selectable). The browser groups them together by their name. Radiobuttons fire change events, but with a catch: When you select one Radiobutton, all other Radiobuttons with the same name, change their state from selected to unselected, but there is only one event.
Therefore with the current client implementation it is not possible to make Radiobuttons self contained.
My proposal would be to change the client to listen to change events on Radiobutton, then search for Radiobuttons with the same name and emulate the change events on them. That would make Radiobuttons self contained.
btw: Nice to hear from you again :)
@SmithChart: I just realized that we probably need both, a backend solution and a frontend solution. If you have self containing Radiobuttons, and you want to check which value is selected, you don't want to have to check all radiobuttons on which one is selected.
Updated proposal: We build these self contained Radiobuttons, and add a widget (RadiobuttonGroup for instance) with a property that goes recursively through its radiobuttons and searches for the selected one. like this you can choose what ever Radiobutton layout you want and can get to the value easily.
JFYI, I've created a class to handle radio buttons group in my app https://github.com/maratori/belote/blob/main/elements/radio_group.py
@fscherf that sounds like a good solution. Fixing the behavior of the browser by emitting the additional change events makes the backend more consistent. (And if in case any browser changes this behavior the quirk could live on the frontend-side, too...) And then build a RadiobuttonGroup as suggested.
@maratori Looking at your code: You are relying on the state of the Radiobuttons for the getters and setters. But also track the value of the last event in self.value. Which one do you use?
@maratori Looking at your code: You are relying on the state of the Radiobuttons for the getters and setters. But also track the value of the last event in
self.value. Which one do you use?
Sorry, I don't understand your question.
In handle_input_event I set self.value (link). This is necessary to sync state on backend with state in browser.
Sorry, I don't understand your question. In
handle_input_eventI setself.value(link). This is necessary to sync state on backend with state in browser.
So you do not use the properties def value() and def values()?
So you do not use the properties
def value()anddef values()?
@SmithChart, @maratori: I implemented a solution that is a combination of my proposal and maratoris backend-side synchronization approach: The RadioButton it self is self-contained, but unchecks all surrounding RadioButtons when it gets checked. I did also build a RadioButtonGroup around RadioButtons that implements properties like value, values and name
https://github.com/lona-web-org/lona/blob/fscherf/topic/radio-buttons/lona/html/data_binding/radio_buttons.py
The RadioButtonsWidget seems a little clumsy to me.
With my implementation I can write a rather short:
rg_source := Radiogroup({
Order.SOURCE_OPENCART: 'Opencart',
Order.SOURCE_EBAY: 'EBay',
Order.SOURCE_OTHER: 'Other',
}, checked=Order.SOURCE_OTHER),
With your suggestion I have to prepare the list of nodes somewhere else. And I have to repeat the same boilerplate for every RadioButtonGroup on the site. This removes the focus from the relevant parts of the code: The actual control flow.
@SmithChart it looks like too specific solution. Such API is not general enough, for me.
@maratori I would argue the other way around. Such generic API does not help when developing an application. Maybe lona.html is not the right place to collect specific solutions.
Independent of the actual implementation i would really like to see any implementation in lona - or at least a chapter in the documentation outlining the problem. So it's clear to a user what to expect and what would be possible ways to fix it.
@SmithChart: from an API standpoint i would agree with @maratori, because your implementation enforces a very specific HTML layout, which won't work for anyone (for example for most my work related usecases regarding radiobuttons)
I don't think this implementation is to clumsy, but I would agree to a shorthand (or a list of shorthands) on RadioButtonGroup that create RadioButtons in a specific way.
This seems to be a very similar problem like #276. I will revisit this when when we decided on the select api
Hi @SmithChart!
First of all, I am sorry this issue has been open for that long time. Radio buttons are weird and for a long time I was not sure how to implement a nice API for them. I have a proposal now (#435)
The API defines two new classes: lona.html.RadioButton and lona.html.RadioGroup. As the name implies, a radio group is a group of radio buttons, and RadioGroup.value always yields the RadioButton.value of the checked radio button of a group.
The API is meant to look and feel like lona.html.Select2, but in HTML a radio group is no element, but a collection of radio buttons that share the same name (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio). The name later serves as a handle for the actual value. In Lona, the name is not necessary, since the python object is the handle for the value, so I tried to create an API where radio button names are supported, but not needed.
The trick is making lona.html.RadioGroup a form internally and giving lona.html.RadioButton a generic default name. This way you can set more specific names, but you don't have to (I think for most cases the generic name is fine).
If radio groups being a form internally is a problem, the tag name can be set as so: RadioButton(tag_name='div')
Full Example
from lona.html import HTML, H1, H2, Div, Label, RadioButton, RadioGroup, Button
from lona import App, View
app = App(__file__)
@app.route('/')
class RadioButtonView(View):
def handle_change(self, input_event):
self.div.set_text(
f'node: {input_event.node.id}, value: {input_event.node.value}',
)
def reset(self, input_event):
self.radio_group_1.value = 1.0
self.radio_group_2.value = 2.1
self.div.set_text('')
def handle_request(self, request):
self.is_daemon = True
self.div = Div()
self.radio_group_1 = RadioGroup(
Label(
'Option 1',
RadioButton(value=1.0, checked=True),
),
Label(
'Option 1.1',
RadioButton(value=1.1),
),
handle_change=self.handle_change,
)
self.radio_group_2 = RadioGroup(
Label(
'Option 2',
RadioButton(value=2.0),
),
Label(
'Option 2.1',
RadioButton(value=2.1, checked=True),
),
handle_change=self.handle_change,
)
return HTML(
H1('RadioButtons and RadioGroups'),
self.div,
H2('RadioGroup 1'),
self.radio_group_1,
H2('RadioGroup 2'),
self.radio_group_2,
H2('Reset'),
Button('Reset', handle_click=self.reset),
)
app.run()