I can bind checkbox

This commit is contained in:
2025-11-07 22:27:32 +01:00
parent e8ecf72205
commit 6a05a84f0c
5 changed files with 108 additions and 39 deletions

View File

@@ -115,7 +115,7 @@ class RadioConverter(DataConverter):
class Binding: class Binding:
def __init__(self, data: Any, attr: str = None): def __init__(self, data: Any, attr: str = None, converter: DataConverter = None):
""" """
Creates a new binding object between a data object and an HTML element. Creates a new binding object between a data object and an HTML element.
The binding is not active until bind_ft() is called. The binding is not active until bind_ft() is called.
@@ -128,12 +128,12 @@ class Binding:
self.htmx_extra = {} self.htmx_extra = {}
self.data = data self.data = data
self.data_attr = attr or get_default_attr(data) self.data_attr = attr or get_default_attr(data)
self.data_converter = converter
# UI-related attributes (configured later via bind_ft) # UI-related attributes (configured later via bind_ft)
self.ft = None self.ft = None
self.ft_name = None self.ft_name = None
self.ft_attr = None self.ft_attr = None
self.data_converter = None
self.detection_mode = DetectionMode.ValueChange self.detection_mode = DetectionMode.ValueChange
self.update_mode = UpdateMode.ValueChange self.update_mode = UpdateMode.ValueChange
@@ -184,15 +184,15 @@ class Binding:
self.ft_attr = attr or get_default_ft_attr(ft) self.ft_attr = attr or get_default_ft_attr(ft)
if is_checkbox(ft): if is_checkbox(ft):
default_data_converter = BooleanConverter() default_data_converter = self.data_converter or BooleanConverter()
default_detection_mode = DetectionMode.AttributePresence default_detection_mode = DetectionMode.AttributePresence
default_update_mode = UpdateMode.AttributePresence default_update_mode = UpdateMode.AttributePresence
elif is_radio(ft): elif is_radio(ft):
default_data_converter = RadioConverter(ft.attrs["value"]) default_data_converter = self.data_converter or RadioConverter(ft.attrs["value"])
default_detection_mode = DetectionMode.ValueChange default_detection_mode = DetectionMode.ValueChange
default_update_mode = UpdateMode.AttributePresence default_update_mode = UpdateMode.AttributePresence
else: else:
default_data_converter = None default_data_converter = self.data_converter
default_detection_mode = DetectionMode.ValueChange default_detection_mode = DetectionMode.ValueChange
default_update_mode = UpdateMode.ValueChange default_update_mode = UpdateMode.ValueChange

View File

@@ -20,6 +20,13 @@ verbs = {
} }
class DoNotSendCls:
pass
DoNotSend = DoNotSendCls()
class TestableElement: class TestableElement:
""" """
Represents an HTML element that can be interacted with in tests. Represents an HTML element that can be interacted with in tests.
@@ -802,7 +809,8 @@ class TestableControl(TestableElement):
def _send_value(self): def _send_value(self):
if self._input_name and self._support_htmx(): if self._input_name and self._support_htmx():
return self._send_htmx_request(data={self._input_name: self.value}) value = {} if self.value is DoNotSend else {self._input_name: self.value}
return self._send_htmx_request(data=value)
return None return None
@@ -824,16 +832,18 @@ class TestableCheckbox(TestableControl):
return self.fields[self._input_name] == True return self.fields[self._input_name] == True
def check(self): def check(self):
self.fields[self._input_name] = True self.fields[self._input_name] = "on"
return self._send_value() return self._send_value()
def uncheck(self): def uncheck(self):
self.fields[self._input_name] = False self.fields[self._input_name] = DoNotSend
return self._send_value() return self._send_value()
def toggle(self): def toggle(self):
self.fields[self._input_name] = not self.fields[self._input_name] if self.fields[self._input_name] == "on":
return self._send_value() return self.uncheck()
else:
return self.check()
class TestableTextarea(TestableControl): class TestableTextarea(TestableControl):

View File

@@ -14,6 +14,7 @@ This test suite covers:
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
import pytest import pytest
from fasthtml.components import ( from fasthtml.components import (
@@ -22,14 +23,14 @@ from fasthtml.components import (
from fasthtml.fastapp import fast_app from fasthtml.fastapp import fast_app
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding from myfasthtml.core.bindings import Binding, BooleanConverter
from myfasthtml.test.matcher import matches, AttributeForbidden from myfasthtml.test.matcher import matches, AttributeForbidden
from myfasthtml.test.testclient import MyTestClient from myfasthtml.test.testclient import MyTestClient
@dataclass @dataclass
class Data: class Data:
value: str = "hello world" value: Any = "hello world"
@dataclass @dataclass
@@ -410,9 +411,9 @@ class TestBindingRadio:
res = binding.update({"radio_name": "option1"}) # option1 is selected res = binding.update({"radio_name": "option1"}) # option1 is selected
expected = [ expected = [
Input(type="radio", name="radio_name", value="option1", checked="true"), Input(type="radio", name="radio_name", value="option1", checked="true", hx_swap_oob="true"),
Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option2"), Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option2", hx_swap_oob="true"),
Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option3"), Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option3", hx_swap_oob="true"),
] ]
assert matches(res, expected) assert matches(res, expected)
@@ -704,3 +705,82 @@ class TestBindingEdgeCases:
testable_input = user.find_element("input") testable_input = user.find_element("input")
testable_input.send("Special: <>&\"'") testable_input.send("Special: <>&\"'")
user.should_see("Special: <>&\"'") user.should_see("Special: <>&\"'")
class TestCheckBox:
def test_i_can_bind_checkbox(self):
data = Data("")
check_box = Input(name="checkbox_name", type="checkbox")
binding = Binding(data)
mk.manage_binding(check_box, binding)
# checkbox is selected
res = binding.update({"checkbox_name": "on"})
expected = [Input(name="checkbox_name", type="checkbox", checked="true", hx_swap_oob="true")]
assert matches(res, expected)
# check box is not selected
res = binding.update({})
expected = [Input(AttributeForbidden("checked"), name="checkbox_name", type="checkbox", hx_swap_oob="true")]
assert matches(res, expected)
def test_checkbox_initial_state_false(self):
data = Data(False)
check_box = Input(name="checkbox_name", type="checkbox")
binding = Binding(data)
updated = mk.manage_binding(check_box, binding)
expected = Input(AttributeForbidden("checked"), name="checkbox_name", type="checkbox", hx_swap_oob="true")
assert matches(updated, expected)
def test_checkbox_initial_state_true(self):
data = Data(True)
check_box = Input(name="checkbox_name", type="checkbox")
binding = Binding(data)
updated = mk.manage_binding(check_box, binding)
expected = Input(name="checkbox_name", type="checkbox", hx_swap_oob="true", checked="true")
assert matches(updated, expected)
def test_i_can_bind_checkbox_and_label_without_converter(self, user, rt):
@rt("/")
def index():
data = Data(True)
input_elt = Input(name="input_name", type="checkbox")
label_elt = Label()
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(label_elt, Binding(data))
return input_elt, label_elt
user.open("/")
user.should_see("True")
testable_input = user.find_element("input")
testable_input.check()
user.should_see("on")
testable_input.uncheck()
user.should_not_see("on")
def test_i_can_bind_checkbox_and_label_with_converter(self, user, rt):
@rt("/")
def index():
data = Data(True)
input_elt = Input(name="input_name", type="checkbox")
label_elt = Label()
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(label_elt, Binding(data, converter=BooleanConverter()))
return input_elt, label_elt
user.open("/")
user.should_see("True")
testable_input = user.find_element("input")
testable_input.check()
user.should_see("True")
testable_input.uncheck()
user.should_see("False")

View File

@@ -6,7 +6,7 @@ from fasthtml.components import Input, Label
from fasthtml.fastapp import fast_app from fasthtml.fastapp import fast_app
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding, DetectionMode, UpdateMode, BooleanConverter from myfasthtml.core.bindings import Binding
from myfasthtml.core.commands import Command, CommandsManager from myfasthtml.core.commands import Command, CommandsManager
from myfasthtml.test.testclient import MyTestClient, TestableElement from myfasthtml.test.testclient import MyTestClient, TestableElement
@@ -78,24 +78,3 @@ class TestingBindings:
testable_input = user.find_element("input") testable_input = user.find_element("input")
testable_input.send("new value") testable_input.send("new value")
user.should_see("new value") # the one from the label user.should_see("new value") # the one from the label
def test_i_can_bind_checkbox(self, user, rt):
@rt("/")
def index():
data = Data(True)
input_elt = Input(name="input_name", type="checkbox")
label_elt = Label()
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(label_elt, Binding(data))
return input_elt, label_elt
user.open("/")
user.should_see("")
testable_input = user.find_element("input")
testable_input.check()
user.should_see("true")
testable_input.uncheck()
user.should_see("false")

View File

@@ -44,7 +44,7 @@ def test_i_can_check_checkbox(test_client, rt):
html = '''<input type="checkbox" name="male" hx_post="/submit"/>''' html = '''<input type="checkbox" name="male" hx_post="/submit"/>'''
@rt('/submit') @rt('/submit')
def post(male: bool): def post(male: bool=None):
return f"Checkbox received {male=}" return f"Checkbox received {male=}"
input_elt = TestableCheckbox(test_client, html) input_elt = TestableCheckbox(test_client, html)
@@ -53,7 +53,7 @@ def test_i_can_check_checkbox(test_client, rt):
assert test_client.get_content() == "Checkbox received male=True" assert test_client.get_content() == "Checkbox received male=True"
input_elt.uncheck() input_elt.uncheck()
assert test_client.get_content() == "Checkbox received male=False" assert test_client.get_content() == "Checkbox received male=None"
input_elt.toggle() input_elt.toggle()
assert test_client.get_content() == "Checkbox received male=True" assert test_client.get_content() == "Checkbox received male=True"