diff --git a/lona/client/_lona/client/input-events.js b/lona/client/_lona/client/input-events.js index 25b95799..000b1b8e 100644 --- a/lona/client/_lona/client/input-events.js +++ b/lona/client/_lona/client/input-events.js @@ -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 diff --git a/lona/client2/_lona/client2/input-events.js b/lona/client2/_lona/client2/input-events.js index c341b1ca..ce7b1fe0 100644 --- a/lona/client2/_lona/client2/input-events.js +++ b/lona/client2/_lona/client2/input-events.js @@ -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 diff --git a/lona/html/__init__.py b/lona/html/__init__.py index 50d34ffe..4bf02077 100644 --- a/lona/html/__init__.py +++ b/lona/html/__init__.py @@ -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 diff --git a/lona/html/nodes/forms/radio_button.py b/lona/html/nodes/forms/radio_button.py new file mode 100644 index 00000000..98af2472 --- /dev/null +++ b/lona/html/nodes/forms/radio_button.py @@ -0,0 +1,136 @@ +from lona.html.nodes.forms.inputs import TextInput +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 get_radio_buttons(self): + return self.query_selector_all('input[type=radio]') + + def get_checked_radio_button(self): + with self.lock: + for radio_button in self.get_radio_buttons(): + if radio_button.checked: + return radio_button + + 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.get_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) + + # 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