Added event handling support to VisNetwork and enhanced component documentation

This commit is contained in:
2025-12-02 20:46:54 +01:00
parent e34d675e38
commit 378775cdf9
12 changed files with 123 additions and 16 deletions

View File

@@ -5,6 +5,10 @@ from myfasthtml.core.network_utils import from_parent_child_list
class CommandsDebugger(SingleInstance):
"""
Represents a debugger designed for visualizing and managing commands in a parent-child
hierarchical structure.
"""
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)

View File

@@ -22,6 +22,12 @@ class DropdownState:
class Dropdown(MultipleInstance):
"""
Represents a dropdown component that can be toggled open or closed. This class is used
to create interactive dropdown elements, allowing for container and button customization.
The dropdown provides functionality to manage its state, including opening, closing, and
handling user interactions.
"""
def __init__(self, parent, content=None, button=None, _id=None):
super().__init__(parent, _id=_id)
self.button = Div(button) if not isinstance(button, FT) else button

View File

@@ -35,6 +35,14 @@ class Commands(BaseCommands):
class FileUpload(MultipleInstance):
"""
Represents a file upload component.
This class provides functionality to handle the uploading process of a file,
extract sheet names from an Excel file, and enables users to select a specific
sheet for further processing. It integrates commands and state management
to ensure smooth operation within a parent application.
"""
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)

View File

@@ -1,5 +1,6 @@
from myfasthtml.controls.Panel import Panel
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.core.commands import LambdaCommand
from myfasthtml.core.instances import SingleInstance, InstancesManager
from myfasthtml.core.network_utils import from_parent_child_list
@@ -11,7 +12,8 @@ class InstancesDebugger(SingleInstance):
def render(self):
nodes, edges = self._get_nodes_and_edges()
vis_network = VisNetwork(self, nodes=nodes, edges=edges, _id="-vis")
c = LambdaCommand(lambda event_data: print("received", event_data))
vis_network = VisNetwork(self, nodes=nodes, edges=edges, _id="-vis", events_handlers={"select_node": c})
return self._panel.set_main(vis_network)
def _get_nodes_and_edges(self):

View File

@@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance
class Keyboard(MultipleInstance):
"""
Represents a keyboard with customizable key combinations support.
The Keyboard class allows managing key combinations and their corresponding
actions for a given parent object.
"""
def __init__(self, parent, combinations=None, _id=None):
super().__init__(parent, _id=_id)
self.combinations = combinations or {}

View File

@@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance
class Mouse(MultipleInstance):
"""
Represents a mechanism to manage mouse event combinations and their associated commands.
This class is used to add, manage, and render mouse event sequences with corresponding
commands, providing a flexible way to handle mouse interactions programmatically.
"""
def __init__(self, parent, _id=None, combinations=None):
super().__init__(parent, _id=_id)
self.combinations = combinations or {}

View File

@@ -38,6 +38,14 @@ class Commands(BaseCommands):
class Panel(MultipleInstance):
"""
Represents a user interface panel that supports customizable left, main, and right components.
The `Panel` class is used to create and manage a panel layout with optional left, main,
and right sections. It provides functionality to set the components of the panel, toggle
sides, and adjust the width of the sides dynamically. The class also handles rendering
the panel with appropriate HTML elements and JavaScript for interactivity.
"""
def __init__(self, parent, conf=None, _id=None):
super().__init__(parent, _id=_id)
self.conf = conf or PanelConf()

View File

@@ -21,6 +21,21 @@ class Commands(BaseCommands):
class Search(MultipleInstance):
"""
Represents a component for managing and filtering a list of items.
It uses fuzzy matching and subsequence matching to filter items.
:ivar items_names: The name of the items used to filter.
:type items_names: str
:ivar items: The first set of items to filter.
:type items: list
:ivar filtered: A copy of the `items` list, representing the filtered items after a search operation.
:type filtered: list
:ivar get_attr: Callable function to extract string values from items for filtering.
:type get_attr: Callable[[Any], str]
:ivar template: Callable function to define how filtered items are rendered.
:type template: Callable[[Any], Any]
"""
def __init__(self,
parent: BaseInstance,
_id=None,

View File

@@ -25,19 +25,33 @@ class VisNetworkState(DbObject):
},
"physics": {"enabled": True}
}
self.events_handlers: dict = {} # {event_name: command_url}
class VisNetwork(MultipleInstance):
def __init__(self, parent, _id=None, nodes=None, edges=None, options=None):
def __init__(self, parent, _id=None, nodes=None, edges=None, options=None, events_handlers=None):
super().__init__(parent, _id=_id)
logger.debug(f"VisNetwork created with id: {self._id}")
self._state = VisNetworkState(self)
self._update_state(nodes, edges, options)
# possible events (expected in snake_case
# - select_node → selectNode
# - select → select
# - click → click
# - double_click → doubleClick
def _update_state(self, nodes, edges, options):
logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}")
if not nodes and not edges and not options:
self._state = VisNetworkState(self)
# Convert Commands to URLs
handlers_htmx_options = {
event_name: command.ajax_htmx_options()
for event_name, command in events_handlers.items()
} if events_handlers else {}
self._update_state(nodes, edges, options, handlers_htmx_options)
def _update_state(self, nodes, edges, options, events_handlers=None):
logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}, {events_handlers=}")
if not nodes and not edges and not options and not events_handlers:
return
state = self._state.copy()
@@ -47,6 +61,8 @@ class VisNetwork(MultipleInstance):
state.edges = edges
if options is not None:
state.options = options
if events_handlers is not None:
state.events_handlers = events_handlers
self._state.update(state)
@@ -70,6 +86,32 @@ class VisNetwork(MultipleInstance):
# Convert Python options to JS
js_options = json.dumps(self._state.options, indent=2)
# Map Python event names to vis-network event names
event_name_map = {
"select_node": "selectNode",
"select": "select",
"click": "click",
"double_click": "doubleClick"
}
# Generate event handlers JavaScript
event_handlers_js = ""
for event_name, command_htmx_options in self._state.events_handlers.items():
vis_event_name = event_name_map.get(event_name, event_name)
event_handlers_js += f"""
network.on('{vis_event_name}', function(params) {{
const event_data = {{
nodes: params.nodes,
edges: params.edges,
pointer: params.pointer
}};
htmx.ajax('POST', '{command_htmx_options['url']}', {{
values: {{event_data: JSON.stringify(event_data)}},
swap: 'none'
}});
}});
"""
return (
Div(
id=self._id,
@@ -92,6 +134,7 @@ class VisNetwork(MultipleInstance):
}};
const options = {js_options};
const network = new vis.Network(container, data, options);
{event_handlers_js}
}})();
""")
)

View File

@@ -97,6 +97,16 @@ class BaseCommand:
def url(self):
return f"{ROUTE_ROOT}{Routes.Commands}?c_id={self.id}"
def ajax_htmx_options(self):
res = {"url": self.url}
if "hx-target" in self._htmx_extra:
res["target"] = self._htmx_extra["hx-target"]
if "hx-swap" in self._htmx_extra:
res["swap"] = self._htmx_extra["hx-swap"]
res["values"] = {}
return res
def get_ft(self):
return self._ft
@@ -143,6 +153,13 @@ class Command(BaseCommand):
return value.split(",")
return value
def ajax_htmx_options(self):
res = super().ajax_htmx_options()
if self.kwargs:
res["values"] |= self.kwargs
res["values"]["c_id"] = f"{self.id}" # cannot be overridden
return res
def execute(self, client_response: dict = None):
ret_from_bindings = []

View File

@@ -1,6 +1,6 @@
import pytest
from myfasthtml.core.instances import SingleInstance
from myfasthtml.core.instances import SingleInstance, InstancesManager
class RootInstanceForTests(SingleInstance):
@@ -25,4 +25,5 @@ def session():
@pytest.fixture(scope="session")
def root_instance(session):
InstancesManager.reset()
return RootInstanceForTests(session=session)

View File

@@ -35,15 +35,6 @@ class TestLayoutBehaviour:
assert layout._main_content == content
assert result == layout # Should return self for chaining
def test_i_can_set_footer_content(self, root_instance):
"""Test setting footer content."""
layout = Layout(root_instance, app_name="Test App")
content = Div("Footer content")
layout.set_footer(content)
assert layout._footer_content == content
def test_i_can_add_content_to_left_drawer(self, root_instance):
"""Test adding content to left drawer."""
layout = Layout(root_instance, app_name="Test App")