I can bind radio

This commit is contained in:
2025-11-07 21:28:19 +01:00
parent cc11e4edaa
commit e8ecf72205
12 changed files with 965 additions and 156 deletions

View File

@@ -7,7 +7,7 @@ from fasthtml.fastapp import fast_app
from myutils.observable import make_observable, bind, collect_return_values, unbind
from myfasthtml.core.constants import Routes, ROUTE_ROOT
from myfasthtml.core.utils import get_default_attr
from myfasthtml.core.utils import get_default_attr, get_default_ft_attr, is_checkbox, is_radio
bindings_app, bindings_rt = fast_app()
logger = logging.getLogger("Bindings")
@@ -61,27 +61,29 @@ class AttrPresentDetection(AttrChangedDetection):
class FtUpdate:
def update(self, ft, ft_name, ft_attr, old, new):
def update(self, ft, ft_name, ft_attr, old, new, converter):
pass
class ValueChangeFtUpdate(FtUpdate):
def update(self, ft, ft_name, ft_attr, old, new):
def update(self, ft, ft_name, ft_attr, old, new, converter):
# simple mode, just update the text or the attribute
new_to_use = converter.convert(new) if converter else new
if ft_attr is None:
ft.children = (new,)
ft.children = (new_to_use,)
else:
ft.attrs[ft_attr] = new
ft.attrs[ft_attr] = new_to_use
return ft
class AttributePresenceFtUpdate(FtUpdate):
def update(self, ft, ft_name, ft_attr, old, new):
def update(self, ft, ft_name, ft_attr, old, new, converter):
# attribute presence mode, toggle the attribute (add or remove it)
new_to_use = converter.convert(new) if converter else new
if ft_attr is None:
ft.children = (bool(new),)
ft.children = (bool(new_to_use),)
else:
ft.attrs[ft_attr] = "true" if new else None # FastHtml auto remove None attributes
ft.attrs[ft_attr] = "true" if new_to_use else None # FastHtml auto remove None attributes
return ft
@@ -104,6 +106,14 @@ class BooleanConverter(DataConverter):
return False
class RadioConverter(DataConverter):
def __init__(self, radio_value):
self.radio_value = radio_value
def convert(self, data):
return data == self.radio_value
class Binding:
def __init__(self, data: Any, attr: str = None):
"""
@@ -136,8 +146,8 @@ class Binding:
def bind_ft(self,
ft,
name,
attr=None,
name=None,
data_converter: DataConverter = None,
detection_mode: DetectionMode = None,
update_mode: UpdateMode = None):
@@ -159,18 +169,37 @@ class Binding:
if self._is_active:
self.deactivate()
if ft.tag in ["input"]:
# I must not force the htmx
if {"hx-post", "hx_post"} & set(ft.attrs.keys()):
raise ValueError(f"Binding '{self.id}': htmx post already set on input.")
# update the component to post on the correct route input and forms only
htmx = self.get_htmx_params()
ft.attrs |= htmx
# Configure UI elements
self.ft = self._safe_ft(ft)
self.ft_name = name
self.ft_attr = attr
self.ft_name = name or ft.attrs.get("name")
self.ft_attr = attr or get_default_ft_attr(ft)
if is_checkbox(ft):
default_data_converter = BooleanConverter()
default_detection_mode = DetectionMode.AttributePresence
default_update_mode = UpdateMode.AttributePresence
elif is_radio(ft):
default_data_converter = RadioConverter(ft.attrs["value"])
default_detection_mode = DetectionMode.ValueChange
default_update_mode = UpdateMode.AttributePresence
else:
default_data_converter = None
default_detection_mode = DetectionMode.ValueChange
default_update_mode = UpdateMode.ValueChange
# Update optional parameters if provided
if data_converter is not None:
self.data_converter = data_converter
if detection_mode is not None:
self.detection_mode = detection_mode
if update_mode is not None:
self.update_mode = update_mode
self.data_converter = data_converter or default_data_converter
self.detection_mode = detection_mode or default_detection_mode
self.update_mode = update_mode or default_update_mode
# Create strategy objects
self._detection = self._factory(self.detection_mode)
@@ -187,6 +216,16 @@ class Binding:
"hx-vals": f'{{"b_id": "{self.id}"}}',
}
def init(self):
"""
Initialise the UI element with the value of the data
:return:
"""
old_value = None # to complicated to retrieve as it depends on the nature of self.ft
new_value = getattr(self.data, self.data_attr)
self.notify(old_value, new_value)
return self
def notify(self, old, new):
"""
Callback when the data attribute changes.
@@ -204,16 +243,21 @@ class Binding:
return None
logger.debug(f"Binding '{self.id}': Changing from '{old}' to '{new}'")
self.ft = self._update.update(self.ft, self.ft_name, self.ft_attr, old, new)
self.ft = self._update.update(self.ft, self.ft_name, self.ft_attr, old, new, self.data_converter)
self.ft.attrs["hx-swap-oob"] = "true"
return self.ft
def update(self, values: dict):
"""
Called by the FastHTML router when a request is received.
:param values:
:return: the list of updated elements (all elements that are bound to this binding)
"""
logger.debug(f"Binding '{self.id}': Updating with {values=}.")
matches, value = self._detection.matches(values)
if matches:
setattr(self.data, self.data_attr, self.data_converter.convert(value) if self.data_converter else value)
setattr(self.data, self.data_attr, value)
res = collect_return_values(self.data)
return res