Skip to content

Commit

Permalink
html: add RadioButton and RadioGroup
Browse files Browse the repository at this point in the history
Signed-off-by: Florian Scherf <mail@florianscherf.de>
  • Loading branch information
fscherf committed Sep 11, 2023
1 parent 7c9e70d commit c974645
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 5 deletions.
67 changes: 67 additions & 0 deletions doc/content/api-reference/html.rst
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,73 @@ select is no multi select, all other options get unselected automatically.
|style |(Dict) contains all styling attributes
RadioGroup and RadioButton
++++++++++++++++++++++++++
.. code-block:: python
from lona.html import RadioGroup, RadioButton, Label
radio_group = RadioGroup(
Label('Option 1', RadioButton(value=1)),
Label('Option 2', RadioButton(value=2.0)),
Label('Option 3', RadioButton(value='3'), checked=True),
)
# adding radio buttons
# `RadioGroup.add_button()` takes any amount of nodes and connects
# the first `Label` with the first `RadioButton` object, using a
# random number and the HTML attribute `for`
radio_group.add_button(Label('Foo'), RadioButton(value='foo'))
radio_group.add_button(Div(Label('Foo'), RadioButton(value='foo')))
# if two non-node values are given, and the first one is a string,
# a `Label` and a `RadioButton` get inserted automatically
radio_group.add_button('Foo', 'foo')
A ``RadioGroup`` consist of one or more ``RadioButton`` and ``Label`` object
pairs, which hold information on value, checked state, and disabled state.
``RadioButton`` objects consist of a value and a checked state. The value can
be anything. If ``RadioButton.render_value`` is set, which is set by default,
the content of ``RadioButton.value`` gets typecasted to a string and rendered
into the HTML tree. This can be disabled if the actually values of the select
shouldn't be disclosed to end users.
``RadioGroup.value`` returns the value of the radio button that is currently
checked.
A radio button can be checked by setting ``RadioGroup.value`` to the value of
the radio button that should be checked, or by setting ``RadioButton.checked``.
**RadioGroup Attributes:**
.. table::
^Name ^Description
|bubble_up |(Bool) Pass input events further
|radio_buttons |(Tuple) tuple of all radio buttons
|checked_radio_button |(Tuple) tuple of all selected options
|value |Value of the currently checked radio button
|values |(Tuple) tuple of all possible values
|id_list |(List) contains all ids
|class_list |(List) contains all classes
|style |(Dict) contains all styling attributes
**RadioButton Attributes:**
.. table::
^Name ^Description
|name |name of the radio button
|value |value of the radio button
|checked |(Bool) sets checked state
|disabled |(Bool) sets the HTML attribute "disabled"
|id_list |(List) contains all ids
|class_list |(List) contains all classes
|style |(Dict) contains all styling attributes
Adding Javascript And CSS To HTML Nodes
---------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions lona/client/_lona/client/input-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class LonaInputEventHandler {
_get_value(node) {
var value = node.value;

// checkbox
if(node.getAttribute('type') == 'checkbox') {
// checkbox / radiobutton
if(node.type == 'checkbox' || node.type == 'radio') {
value = node.checked;

// select
Expand Down
4 changes: 2 additions & 2 deletions lona/client2/_lona/client2/input-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class LonaInputEventHandler {
_get_value(node) {
var value = node.value;

// checkbox
if(node.getAttribute('type') == 'checkbox') {
// checkbox / radiobutton
if(node.type == 'checkbox' || node.type == 'radio') {
value = node.checked;

// select
Expand Down
1 change: 1 addition & 0 deletions lona/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
Base,
)
from lona.html.nodes.interactive_elements import Summary, Details, Dialog
from lona.html.nodes.forms.radio_button import RadioButton, RadioGroup
from lona.html.nodes.scripting import NoScript, Script, Canvas
from lona.html.nodes.forms.select2 import Select2, Option2
from lona.html.nodes.web_components import Template, Slot
Expand Down
198 changes: 198 additions & 0 deletions lona/html/nodes/forms/radio_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
from uuid import uuid1

from lona.html.nodes.forms.inputs import TextInput
from lona.html.nodes.text_content import Div
from lona.html.nodes.forms import Label
from lona.html.node import Node


class RadioButton(TextInput):
INPUT_ATTRIBUTE_NAME = 'checked'

ATTRIBUTES = {
'type': 'radio',
'name': 'radio',
}

def __init__(
self,
*args,
value='',
bubble_up=True,
checked=False,
render_value=True,
**kwargs,
):

self.render_value = render_value

super().__init__(*args, bubble_up=bubble_up, **kwargs)

self.value = value
self.checked = checked

def _render_value(self, value):
return str(value)

# value
@property
def value(self):
return self._value

@value.setter
def value(self, new_value):
with self.lock:
if self.render_value:
self.attributes['value'] = self._render_value(new_value)

self._value = new_value

# name
@property
def name(self):
return self.attributes['name']

@name.setter
def name(self, new_value):
self.attributes['name'] = new_value

# checked
@property
def checked(self):
return 'checked' in self.attributes

@checked.setter
def checked(self, new_value):
if new_value:
self.attributes['checked'] = ''

else:
del self.attributes['checked']


class RadioGroup(Node):
TAG_NAME = 'form'

def handle_input_event(self, input_event):

# check if incoming event was fired by a radio button and bubble it
# up if not
if input_event.name != 'change':
return super().handle_input_event(input_event)

if (not input_event.node or
input_event.node.tag_name != 'input' or
input_event.node.attributes.get('type', '') != 'radio'):

return super().handle_input_event(input_event)

# uncheck all radio buttons in the same radio group that are unchecked
# on the client
with self.lock:
name = input_event.node.attributes.get('name', '')

for radio_button in self.radio_buttons:
if radio_button is input_event.node:
continue

if radio_button.attributes.get('name', '') != name:
continue

# The browser unchecks all previously checked radio buttons
# in the same radio group autamatically. So we don't need
# to send a patch to the original issuer of the change event.
if 'checked' in radio_button.attributes:
radio_button.attributes.__delitem__(
'checked',
issuer=(input_event.connection, input_event.window_id),
)

# patch input_event so `input_event.node.value` and `input_event.data`
# yield the actual value of the radio group
input_event.node = self
input_event.data = self.value

return super().handle_input_event(input_event)

@property
def radio_buttons(self):
return tuple(self.query_selector_all('input[type=radio]'))

@property
def checked_radio_button(self):
with self.lock:
for radio_button in self.radio_buttons:
if radio_button.checked:
return radio_button

# value
@property
def value(self):
checked_radio_button = self.checked_radio_button

if not checked_radio_button:
return

return checked_radio_button.value

@value.setter
def value(self, new_value):
with self.lock:
radio_buttons = self.radio_buttons

# check if value is available
if new_value is not None:
values = []

for radio_button in radio_buttons:
values.append(radio_button.value)

if new_value not in values:
raise ValueError(
f"no radio button with value '{new_value}'",
)

# update all radio button checked state
for radio_button in radio_buttons:
radio_button.checked = radio_button.value == new_value

# values
@property
def values(self):
values = []

with self.lock:
for radio_button in self.radio_buttons:
values.append(radio_button.value)

return tuple(values)

def add_button(self, *nodes):

# string value pair
if (len(nodes) == 2 and
isinstance(nodes[0], str) and
not isinstance(nodes[1], Node)):

return self.add_button(
Label(nodes[0]),
RadioButton(value=nodes[1]),
)

# find labels and radio buttons
helper_node = Div(nodes)

labels = helper_node.query_selector_all('label')
buttons = helper_node.query_selector_all('input[type=radio]')

# attach autogenerated id so the labels get clickable in the browser
button_id = f'id_{uuid1().hex}'

for label in labels:
label.attributes['for'] = button_id

for button in buttons:
button.id_list.add(button_id)

# add all nodes to button group
self.nodes.extend(nodes)
22 changes: 21 additions & 1 deletion test_project/views/events/change_events.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from pprint import pformat

from lona.html import TextInput, CheckBox, Pre, Div, H2
from lona.html import (
RadioButton,
RadioGroup,
TextInput,
CheckBox,
Label,
Pre,
Div,
H2,
)
from lona.view import View


Expand All @@ -25,6 +34,17 @@ def handle_request(self, request):
bubble_up=True,
),
),
RadioGroup(
Label(
'Option 1',
RadioButton(value='option-1'),
),
Label(
'Option 2',
RadioButton(value='option-2'),
),
bubble_up=True,
),
style={
'float': 'left',
'width': '50%',
Expand Down
17 changes: 17 additions & 0 deletions test_project/views/events/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import html

from lona.html import (
RadioButton,
NumberInput,
RadioGroup,
TextInput,
TextArea,
CheckBox,
Expand All @@ -12,6 +14,7 @@
Select,
Button,
Table,
Label,
FOCUS,
CLICK,
HTML,
Expand Down Expand Up @@ -73,6 +76,7 @@ def prefill_inputs(self, input_event=None):
self.select2.value = 2.0
self.multi_select.value = ['1', '2.0']
self.multi_select2.value = [1, 2.0]
self.radio_group.value = 2.0

def reset_inputs(self, input_event=None):
return RedirectResponse('.')
Expand Down Expand Up @@ -178,6 +182,14 @@ def handle_request(self, request):
_id='multi-select-2',
)

self.radio_group = RadioGroup(
Div(Label('Option 1', RadioButton(value=1))),
Div(Label('Option 2', RadioButton(value=2.0))),
Div(Label('Option 3', RadioButton(value=3))),
handle_change=self.handle_event,
_id='radio-group',
)

# focus / blur nodes
self.text_input_focus = TextInput(
events=[FOCUS],
Expand Down Expand Up @@ -246,6 +258,10 @@ def handle_request(self, request):
Td('Multi Select2', width='200px'),
Td(self.multi_select2),
),
Tr(
Td('Radio Group', width='200px'),
Td(self.radio_group),
),
),

H3('Focus Events'),
Expand Down Expand Up @@ -316,6 +332,7 @@ def handle_request(self, request):
'#select-2': self.select2,
'#multi-select': self.multi_select,
'#multi-select-2': self.multi_select2,
'#radio-group': self.radio_group,

# focus / blur nodes
'#text-input-focus': self.text_input_focus,
Expand Down
Loading

0 comments on commit c974645

Please sign in to comment.