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

html: add radio buttons #435

Merged
merged 4 commits into from
Oct 2, 2023
Merged

html: add radio buttons #435

merged 4 commits into from
Oct 2, 2023

Conversation

fscherf
Copy link
Member

@fscherf fscherf commented Jul 7, 2023

This PR adds initial support for HTML radio buttons (#229)

@codecov-commenter
Copy link

codecov-commenter commented Jul 7, 2023

Codecov Report

Patch coverage: 89.09% and project coverage change: +0.13% 🎉

Comparison is base (50f2c78) 72.63% compared to head (c974645) 72.77%.
Report is 8 commits behind head on master.

❗ Current head c974645 differs from pull request most recent head bd7638e. Consider uploading reports for the commit bd7638e to get more accurate results

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #435      +/-   ##
==========================================
+ Coverage   72.63%   72.77%   +0.13%     
==========================================
  Files          84       85       +1     
  Lines        5760     5866     +106     
  Branches     1238      959     -279     
==========================================
+ Hits         4184     4269      +85     
- Misses       1311     1329      +18     
- Partials      265      268       +3     
Files Changed Coverage Δ
lona/html/nodes/forms/radio_button.py 88.34% <88.34%> (ø)
lona/html/__init__.py 100.00% <100.00%> (ø)
lona/html/attribute_dict.py 71.17% <100.00%> (+1.07%) ⬆️

... and 13 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@SmithChart SmithChart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see some progress here :)

I like that even the RadioGroup feels like an HTML element in this context (even if HTML does not specify such node).

Do you see some room for a (specific) high-level interface like the one suggested here in Lona? Or should such interfaces live somewhere else? It feels wrong to have a custom widget in every project.

Comment on lines 122 to 157
# value
@property
def value(self):
checked_radio_button = self.get_checked_radio_button()

if not checked_radio_button:
return

return checked_radio_button.value

@value.setter
def value(self, new_value):
with self.lock:
for radio_button in self.get_radio_buttons():
radio_button.checked = radio_button.value == new_value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess returning None if no RadioButton is checked is a good way to handle this state.
So in the same way setting value = None creates a list where no RadioButton is checked.

But: If I try to set a value that is not part of a RadioGroup I would suggest to raise an ValueError instead of silently not checking any RadioButton.



class RadioGroup(Node):
TAG_NAME = 'form'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML5 specification explicitly does not allow nesting of <form>. Even if it feels like there are not many use-cases where one want's to use actual HTML forms in Lona. Why make using them extra hard?

Is there any gain from using form here instead of an element like div that is intended for nesting?

@fscherf
Copy link
Member Author

fscherf commented Jul 9, 2023

Nice to see some progress here :)

Yeah, sorry :D

Do you see some room for a (specific) high-level interface like the one suggested here in Lona? Or should such interfaces live somewhere else? It feels wrong to have a custom widget in every project.

I was thinking about something like lona.html.RadioGroup.add_button() too, but I am not sure which HTML layout such a method should produce. In picocss the default layout seems to be this:

<label>
    My Label
    <input type="radio" value="my-value">
</label>

This seems to work fine in all cases I encountered but the "more HTML5" layout would be like this:

<label for="my-radio-button>My Label</label>
<input type="radio" id="my-radio-button" value="my-value">

[...] If I try to set a value that is not part of a RadioGroup I would suggest to raise an ValueError instead of silently not checking any RadioButton.

I agree. lona.html.Select2 implements exactly this behavior so this should be consistent anyway.

The HTML5 specification explicitly does not allow nesting of <form>. Even if it feels like there are not many use-cases where one want's to use actual HTML forms in Lona. Why make using them extra hard?

Is there any gain from using form here instead of an element like div that is intended for nesting?

Unfortunately, radio groups have no dedicated HTML tag but are identified by multiple radio buttons using the same name. When using a div as radio group, your code would have to look like this:

RadioGroup(
    RadioButton(value='foo', name='my-group'),
    RadioButton(value='bar', name='my-group'),
    RadioButton(value='baz', name='my-group', checkd=True),
)```

If possible I don't want a mandatory name field that is not used by the application code since the `RadioGroup` object is the handle to the checked value. When using a `<form>` `RadioButton.name` can be generic, when using `<div>` not.

@SmithChart
Copy link
Contributor

I was thinking about something like lona.html.RadioGroup.add_button() too, but I am not sure which HTML layout such a method should produce. In picocss the default layout seems to be this:

<label>
    My Label
    <input type="radio" value="my-value">
</label>

This seems to work fine in all cases I encountered but the "more HTML5" layout would be like this:

<label for="my-radio-button>My Label</label>
<input type="radio" id="my-radio-button" value="my-value">

So, if both work, why care which one to use? (But I personally would prefer to stick closer to what HTML5 suggests.)
In my use-cases the choices for a RadioGroup often come from a (filtered) Django DB choice. Having add_button() would reduce boilerplate to:

choice = (("v1", "Display Value V2"), ("v2", "Display Value V2")) # already in the DB model

rg = RadioGroup()
[rg.add_button(v, k) for k,v in l]

Unfortunately, radio groups have no dedicated HTML tag but are identified by multiple radio buttons using the same name. When using a div as radio group, your code would have to look like this:

RadioGroup(
    RadioButton(value='foo', name='my-group'),
    RadioButton(value='bar', name='my-group'),
    RadioButton(value='baz', name='my-group', checkd=True),
)```

If possible I don't want a mandatory name field that is not used by the application code since the `RadioGroup` object is the handle to the checked value. When using a `<form>` `RadioButton.name` can be generic, when using `<div>` not.

OK, and when using a form instead of a div the default name is enough to group the RadioButtons, I see.
In this case using form is better than having some hidden magic in RadioGroup that takes care of the renaming.

@fscherf
Copy link
Member Author

fscherf commented Jul 10, 2023

So, if both work, why care which one to use? (But I personally would prefer to stick closer to what HTML5 suggests.)
In my use-cases the choices for a RadioGroup often come from a (filtered) Django DB choice. Having add_button() would reduce boilerplate to: [...]

Fair enough. If we decide on a RadioGroup.add_button() I would want to use the second layout, the more HTML5-ish, too. I have a second concern though: We have lona.html.Select and lona.html.Select2. In Lona 2, lona.html.Select will be fully replaced by lona.html.Select2 so I am only concerned with this implementation.

lona.html.Select used this API to define options:

from lona.html import Select

Select(
    values=[
        # value, label, is_selected
        ('foo', 'Foo', True),
        ('bar', 'Bar', False),
    ],
)

The comment pretty much marks the problem: It is easy to get the order of the options wrong, and there are cryptic flags needed to set properties on the resulting lona.html.Option objects. This works ok-ish for a few options and arguments but becomes a problem if you add more arguments. Also adding HTML attributes, or style attributes is a problem.

Therefore lona.html.Select2.add_option() takes whole lona.html.Option2 objects, so code like this is possible:

s = Select2()

s.add_option(Option2('Option 1', value='option-1', selected=True))
s.add_option(Option2(Span('Option 2'), value='option-2', style={'color': 'red'}))

It's more verbose, but also more flexible.

To be consistent with this API lona.html.RadioGroup.add_button() would have to look like this:

r = RadioGroup()

r.add_button(
    Label('Option 1', style={'color': 'red'}),
    RadioButton(value='option-1'),
)

r.add_button(
    Div(  # using a div to make the listing go top to bottom instead left to right
        Label('Option 1', style={'color': 'red'}),
        RadioButton(value='option-1'),
    ),
)

The add_button then could search for any given labels and set the for attribute, so clicking in the browser works as expected.

@fscherf
Copy link
Member Author

fscherf commented Aug 13, 2023

@SmithChart: I added an implementation for RadioGroup.add_button(). The method takes a variable list of nodes, searches for Label and RadioButton objects, and links them together, using the for attribute. So all of these calls work:

radio_group.add_button(Label('foo'), RadioButton(value='foo'))
radio_group.add_button(Div(Label('foo'), RadioButton(value='foo')))
radio_group.add_button(Label('foo', RadioButton(value='foo')))

For convenience, I added a special case that makes code like this work too:

radio_group.add_button('Foo', 'foo')

I am not sure if this is too much magic, but it seems to be useful. What do you think?

@fscherf fscherf linked an issue Aug 14, 2023 that may be closed by this pull request
@SmithChart
Copy link
Contributor

@SmithChart: I added an implementation for RadioGroup.add_button(). The method takes a variable list of nodes, searches for Label and RadioButton objects, and links them together, using the for attribute. So all of these calls work:

radio_group.add_button(Label('foo'), RadioButton(value='foo'))
radio_group.add_button(Div(Label('foo'), RadioButton(value='foo')))
radio_group.add_button(Label('foo', RadioButton(value='foo')))

For convenience, I added a special case that makes code like this work too:

radio_group.add_button('Foo', 'foo')

I am not sure if this is too much magic, but it seems to be useful. What do you think?

I get the point on magic arguments and flags you made for the Select.
For a RadioButton I think two magic arguments are still acceptable. This should be easily discoverable. And the long form from above gives full flexibility.

So: I think I like this API.

@fscherf
Copy link
Member Author

fscherf commented Sep 6, 2023

@SmithChart: Great! Thanks for your opinion on that. I will finish this PR, update the docs, and ping you for the final review

Signed-off-by: Florian Scherf <mail@florianscherf.de>
Signed-off-by: Florian Scherf <mail@florianscherf.de>
With the `issuer` argument it is possible to make changes that don't get rolled
out to the original issuer of the change. In this particular case this is meant
to be used for radio buttons to synchronize the `checked` property on the
backend with the frontend.

Signed-off-by: Florian Scherf <mail@florianscherf.de>
@fscherf fscherf marked this pull request as ready for review September 11, 2023 19:36
Copy link
Contributor

@SmithChart SmithChart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fscherf I've reviewed your changes and gave it a try on a project I had open.

@@ -1,5 +1,5 @@
SHELL=/bin/bash
PYTHON=python3.10
PYTHON=python3.11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily, but it isn't a problem either because it is a separate patch. While implementing this, I ran into some issues and wanted to use the new exception pretty printing of Python 3.11.

radio_group = RadioGroup(
Label('Option 1', RadioButton(value=1)),
Label('Option 2', RadioButton(value=2.0)),
Label('Option 3', RadioButton(value='3'), checked=True),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked=True is an argument for the RadioButton, not the Label.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed.

Comment on lines 837 to 841
radio_group = RadioGroup(
Label('Option 1', RadioButton(value=1)),
Label('Option 2', RadioButton(value=2.0)),
Label('Option 3', RadioButton(value='3'), checked=True),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing a RadioGroup via the constructor is not implemented! Using it this way Labels and RadioButtons are not linked together with the for= attribute.
Using radio_group.add_button() works however.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is due to the weird implementation of radio buttons in HTML. Correlating which Label is linked to which RadioButton is tricky when you get a whole tree of both. That's why, in this example, the RadioButton objects are inside of the Label objects. That way they are linked together by the browser implicitly. In add_button() that's not a problem, because you are supposed to provide only one label and one input with each call.

The RadioGroup.value is initialized correctly in this example because value is a property that searches for a RadioButton that is checked on every call. It is not a static value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Good point. Having the nodes in this way makes the result look funny. But add_button() works as expected. So no further action needed here.

Signed-off-by: Florian Scherf <mail@florianscherf.de>
@fscherf
Copy link
Member Author

fscherf commented Sep 27, 2023

@SmithChart: This PR should be ready to merge now I think.

Copy link
Contributor

@SmithChart SmithChart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@fscherf feel free to close the resolved discussions. I can't do that from here.

@fscherf
Copy link
Member Author

fscherf commented Oct 2, 2023

@SmithChart: Great! Thanks a lot for your help on this topic, and for your patience. I know it took a long time.

@fscherf fscherf merged commit 8ee2986 into master Oct 2, 2023
5 checks passed
@fscherf fscherf deleted the fscherf/radio-buttons branch October 2, 2023 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Groups of radioboxes: Not all Python-Objects updated correctly?
3 participants