Compare commits
2 Commits
dc2f6fd04a
...
4b86194c7e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b86194c7e | |||
| 408c8332dc |
@@ -36,7 +36,7 @@ markdown-it-py==4.0.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==10.8.0
|
||||
myauth==0.2.0
|
||||
myutils==0.1.0
|
||||
myutils==0.4.0
|
||||
nh3==0.3.1
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
|
||||
@@ -30,8 +30,7 @@ class mk:
|
||||
@staticmethod
|
||||
def manage_command(ft, command: Command):
|
||||
if command:
|
||||
htmx = command.get_htmx_params()
|
||||
ft.attrs |= htmx
|
||||
ft = command.bind_ft(ft)
|
||||
|
||||
return ft
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from myutils.observable import NotObservableError, ObservableEvent, add_event_listener, remove_event_listener
|
||||
|
||||
from myfasthtml.core.constants import Routes, ROUTE_ROOT
|
||||
|
||||
|
||||
@@ -25,13 +27,14 @@ class BaseCommand:
|
||||
self.id = uuid.uuid4()
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.htmx_extra = {}
|
||||
self._htmx_extra = {}
|
||||
self._bindings = []
|
||||
|
||||
# register the command
|
||||
CommandsManager.register(self)
|
||||
|
||||
def get_htmx_params(self):
|
||||
return self.htmx_extra | {
|
||||
return self._htmx_extra | {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
||||
}
|
||||
@@ -39,9 +42,46 @@ class BaseCommand:
|
||||
def execute(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def htmx(self, target=None):
|
||||
if target:
|
||||
self.htmx_extra["hx-target"] = target
|
||||
def htmx(self, target="this", swap="innerHTML"):
|
||||
if target is None:
|
||||
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
|
||||
|
||||
|
||||
@@ -71,7 +111,26 @@ class Command(BaseCommand):
|
||||
self.kwargs = kwargs
|
||||
|
||||
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):
|
||||
return f"Command({self.name})"
|
||||
|
||||
@@ -27,15 +27,15 @@ class Data:
|
||||
|
||||
def add_suggestion():
|
||||
nb = len(data.value)
|
||||
data.value.append(f"suggestion{nb}")
|
||||
data.value = data.value + [f"suggestion{nb}"]
|
||||
|
||||
|
||||
def remove_suggestion():
|
||||
if len(data.value) > 0:
|
||||
data.value.pop()
|
||||
data.value = data.value[:-1]
|
||||
|
||||
|
||||
data = Data(["suggestion1", "suggestion2", "suggestion3"])
|
||||
data = Data(["suggestion0", "suggestion1", "suggestion2"])
|
||||
|
||||
|
||||
@rt("/")
|
||||
@@ -46,11 +46,11 @@ def get():
|
||||
input_elt = Input(name="input_name", list="suggestions")
|
||||
label_elt = Label()
|
||||
|
||||
mk.manage_binding(input_elt, Binding(data))
|
||||
mk.manage_binding(datalist, Binding(data))
|
||||
mk.manage_binding(label_elt, Binding(data))
|
||||
|
||||
add_button = mk.button("Add", command=Command("Add", "Add a suggestion", add_suggestion))
|
||||
remove_button = mk.button("Remove", command=Command("Remove", "Remove a suggestion", remove_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).bind(data))
|
||||
|
||||
return Div(
|
||||
add_button,
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fasthtml.components import Button
|
||||
from myutils.observable import make_observable, bind
|
||||
|
||||
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():
|
||||
@@ -23,3 +35,61 @@ def test_i_can_create_a_command_with_no_params():
|
||||
def test_command_are_registered():
|
||||
command = Command('test', 'Command description', callback)
|
||||
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