Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Groups of radioboxes: Not all Python-Objects updated correctly? #229

Closed
SmithChart opened this issue Aug 9, 2022 · 14 comments · Fixed by #435
Closed

Groups of radioboxes: Not all Python-Objects updated correctly? #229

SmithChart opened this issue Aug 9, 2022 · 14 comments · Fixed by #435
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@SmithChart
Copy link
Contributor

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 :-)

@fscherf
Copy link
Member

fscherf commented Aug 11, 2022

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 :)

@fscherf fscherf added enhancement New feature or request help wanted Extra attention is needed labels Aug 11, 2022
@fscherf
Copy link
Member

fscherf commented Aug 11, 2022

@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.

@maratori
Copy link
Member

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

@SmithChart
Copy link
Contributor Author

@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
Copy link
Member

@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.

@SmithChart
Copy link
Contributor Author

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.

So you do not use the properties def value() and def values()?

@maratori
Copy link
Member

So you do not use the properties def value() and def values()?

No, I get value here and set it here.

@fscherf
Copy link
Member

fscherf commented Aug 23, 2022

@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

@SmithChart
Copy link
Contributor Author

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.

@maratori
Copy link
Member

maratori commented Sep 6, 2022

@SmithChart it looks like too specific solution. Such API is not general enough, for me.

@SmithChart
Copy link
Contributor Author

SmithChart commented Sep 10, 2022

@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.

@fscherf
Copy link
Member

fscherf commented Sep 10, 2022

@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.

@fscherf
Copy link
Member

fscherf commented Oct 31, 2022

This seems to be a very similar problem like #276. I will revisit this when when we decided on the select api

@fscherf
Copy link
Member

fscherf commented Jul 7, 2023

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()                                                                      

Example Screencap

radio-buttons

@fscherf fscherf linked a pull request Aug 14, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants