I can attach a command with a binding
This commit is contained in:
@@ -36,7 +36,7 @@ markdown-it-py==4.0.0
|
|||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
more-itertools==10.8.0
|
more-itertools==10.8.0
|
||||||
myauth==0.2.0
|
myauth==0.2.0
|
||||||
myutils==0.1.0
|
myutils==0.4.0
|
||||||
nh3==0.3.1
|
nh3==0.3.1
|
||||||
oauthlib==3.3.1
|
oauthlib==3.3.1
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ class mk:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def manage_command(ft, command: Command):
|
def manage_command(ft, command: Command):
|
||||||
if command:
|
if command:
|
||||||
htmx = command.get_htmx_params()
|
ft = command.bind_ft(ft)
|
||||||
ft.attrs |= htmx
|
|
||||||
|
|
||||||
return ft
|
return ft
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from myutils.observable import NotObservableError, ObservableEvent, add_event_listener, remove_event_listener
|
||||||
|
|
||||||
from myfasthtml.core.constants import Routes, ROUTE_ROOT
|
from myfasthtml.core.constants import Routes, ROUTE_ROOT
|
||||||
|
|
||||||
|
|
||||||
@@ -25,13 +27,14 @@ class BaseCommand:
|
|||||||
self.id = uuid.uuid4()
|
self.id = uuid.uuid4()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.htmx_extra = {}
|
self._htmx_extra = {}
|
||||||
|
self._bindings = []
|
||||||
|
|
||||||
# register the command
|
# register the command
|
||||||
CommandsManager.register(self)
|
CommandsManager.register(self)
|
||||||
|
|
||||||
def get_htmx_params(self):
|
def get_htmx_params(self):
|
||||||
return self.htmx_extra | {
|
return self._htmx_extra | {
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||||
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
||||||
}
|
}
|
||||||
@@ -39,9 +42,46 @@ class BaseCommand:
|
|||||||
def execute(self):
|
def execute(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def htmx(self, target=None):
|
def htmx(self, target="this", swap="innerHTML"):
|
||||||
if target:
|
if target is None:
|
||||||
self.htmx_extra["hx-target"] = target
|
self._htmx_extra["hx-swap"] = "none"
|
||||||
|
elif target != "this":
|
||||||
|
self._htmx_extra["hx-target"] = target
|
||||||
|
|
||||||
|
if swap is None:
|
||||||
|
self._htmx_extra["hx-swap"] = "none"
|
||||||
|
elif swap != "innerHTML":
|
||||||
|
self._htmx_extra["hx-swap"] = swap
|
||||||
|
return self
|
||||||
|
|
||||||
|
def bind_ft(self, ft):
|
||||||
|
"""
|
||||||
|
Update the FT with the command's HTMX parameters.
|
||||||
|
|
||||||
|
:param ft:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
htmx = self.get_htmx_params()
|
||||||
|
ft.attrs |= htmx
|
||||||
|
return ft
|
||||||
|
|
||||||
|
def bind(self, data):
|
||||||
|
"""
|
||||||
|
Attach a binding to the command.
|
||||||
|
When done, if a binding is triggered during the execution of the command,
|
||||||
|
the results of the binding will be passed to the command's execute() method.
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not hasattr(data, '_listeners'):
|
||||||
|
raise NotObservableError(
|
||||||
|
f"Object must be made observable with make_observable() before binding"
|
||||||
|
)
|
||||||
|
self._bindings.append(data)
|
||||||
|
|
||||||
|
# by default, remove the swap on the attached element when binding is used
|
||||||
|
self._htmx_extra["hx-swap"] = "none"
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +111,26 @@ class Command(BaseCommand):
|
|||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
return self.callback(*self.args, **self.kwargs)
|
ret_from_bindings = []
|
||||||
|
|
||||||
|
def binding_result_callback(attr, old, new, results):
|
||||||
|
ret_from_bindings.extend(results)
|
||||||
|
|
||||||
|
for data in self._bindings:
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
||||||
|
|
||||||
|
ret = self.callback(*self.args, **self.kwargs)
|
||||||
|
|
||||||
|
for data in self._bindings:
|
||||||
|
remove_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
||||||
|
|
||||||
|
if not ret_from_bindings:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if isinstance(ret, list):
|
||||||
|
return ret + ret_from_bindings
|
||||||
|
else:
|
||||||
|
return [ret] + ret_from_bindings
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Command({self.name})"
|
return f"Command({self.name})"
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ class Data:
|
|||||||
|
|
||||||
def add_suggestion():
|
def add_suggestion():
|
||||||
nb = len(data.value)
|
nb = len(data.value)
|
||||||
data.value.append(f"suggestion{nb}")
|
data.value = data.value + [f"suggestion{nb}"]
|
||||||
|
|
||||||
|
|
||||||
def remove_suggestion():
|
def remove_suggestion():
|
||||||
if len(data.value) > 0:
|
if len(data.value) > 0:
|
||||||
data.value.pop()
|
data.value = data.value[:-1]
|
||||||
|
|
||||||
|
|
||||||
data = Data(["suggestion1", "suggestion2", "suggestion3"])
|
data = Data(["suggestion1", "suggestion2", "suggestion3"])
|
||||||
@@ -49,8 +49,8 @@ def get():
|
|||||||
mk.manage_binding(input_elt, Binding(data))
|
mk.manage_binding(input_elt, Binding(data))
|
||||||
mk.manage_binding(label_elt, Binding(data))
|
mk.manage_binding(label_elt, Binding(data))
|
||||||
|
|
||||||
add_button = mk.button("Add", command=Command("Add", "Add a suggestion", add_suggestion))
|
add_button = mk.button("Add", command=Command("Add", "Add a suggestion", add_suggestion).bind(data))
|
||||||
remove_button = mk.button("Remove", command=Command("Remove", "Remove a suggestion", remove_suggestion))
|
remove_button = mk.button("Remove", command=Command("Remove", "Remove a suggestion", remove_suggestion).bind(data))
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
add_button,
|
add_button,
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from fasthtml.components import Button
|
||||||
|
from myutils.observable import make_observable, bind
|
||||||
|
|
||||||
from myfasthtml.core.commands import Command, CommandsManager
|
from myfasthtml.core.commands import Command, CommandsManager
|
||||||
|
from myfasthtml.core.constants import ROUTE_ROOT, Routes
|
||||||
|
from myfasthtml.test.matcher import matches
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Data:
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
def callback():
|
def callback():
|
||||||
@@ -23,3 +35,61 @@ def test_i_can_create_a_command_with_no_params():
|
|||||||
def test_command_are_registered():
|
def test_command_are_registered():
|
||||||
command = Command('test', 'Command description', callback)
|
command = Command('test', 'Command description', callback)
|
||||||
assert CommandsManager.commands.get(str(command.id)) is command
|
assert CommandsManager.commands.get(str(command.id)) is command
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_bind_a_command_to_an_element():
|
||||||
|
command = Command('test', 'Command description', callback)
|
||||||
|
elt = Button()
|
||||||
|
updated = command.bind_ft(elt)
|
||||||
|
|
||||||
|
expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}")
|
||||||
|
|
||||||
|
assert matches(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_suppress_swapping_with_target_attr():
|
||||||
|
command = Command('test', 'Command description', callback).htmx(target=None)
|
||||||
|
elt = Button()
|
||||||
|
updated = command.bind_ft(elt)
|
||||||
|
|
||||||
|
expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}", hx_swap="none")
|
||||||
|
|
||||||
|
assert matches(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_bind_a_command_to_an_observable():
|
||||||
|
data = Data("hello")
|
||||||
|
|
||||||
|
def on_data_change(old, new):
|
||||||
|
return old, new
|
||||||
|
|
||||||
|
def another_callback():
|
||||||
|
data.value = "new value"
|
||||||
|
return "another callback result"
|
||||||
|
|
||||||
|
make_observable(data)
|
||||||
|
bind(data, "value", on_data_change)
|
||||||
|
command = Command('test', 'Command description', another_callback).bind(data)
|
||||||
|
|
||||||
|
res = command.execute()
|
||||||
|
|
||||||
|
assert res == ["another callback result", ("hello", "new value")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_bind_a_command_to_an_observable_2():
|
||||||
|
data = Data("hello")
|
||||||
|
|
||||||
|
def on_data_change(old, new):
|
||||||
|
return old, new
|
||||||
|
|
||||||
|
def another_callback():
|
||||||
|
data.value = "new value"
|
||||||
|
return ["another 1", "another 2"]
|
||||||
|
|
||||||
|
make_observable(data)
|
||||||
|
bind(data, "value", on_data_change)
|
||||||
|
command = Command('test', 'Command description', another_callback).bind(data)
|
||||||
|
|
||||||
|
res = command.execute()
|
||||||
|
|
||||||
|
assert res == ["another 1", "another 2", ("hello", "new value")]
|
||||||
|
|||||||
Reference in New Issue
Block a user