Implemented lazy loading
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@@ -11,6 +12,7 @@ from myfasthtml.core.utils import flatten
|
||||
|
||||
logger = logging.getLogger("Commands")
|
||||
|
||||
AUTO_SWAP_OOB = "__auto_swap_oob__"
|
||||
|
||||
class Command:
|
||||
"""
|
||||
@@ -71,7 +73,7 @@ class Command:
|
||||
self.callback = callback
|
||||
self.default_args = args or []
|
||||
self.default_kwargs = kwargs or {}
|
||||
self._htmx_extra = {}
|
||||
self._htmx_extra = {AUTO_SWAP_OOB: True}
|
||||
self._bindings = []
|
||||
self._ft = None
|
||||
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
||||
@@ -97,7 +99,7 @@ class Command:
|
||||
def get_key(self):
|
||||
return self._key
|
||||
|
||||
def get_htmx_params(self):
|
||||
def get_htmx_params(self, escaped=False):
|
||||
res = {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||
"hx-swap": "outerHTML",
|
||||
@@ -115,10 +117,13 @@ class Command:
|
||||
# kwarg are given to the callback as values
|
||||
res["hx-vals"] |= self.default_kwargs
|
||||
|
||||
if escaped:
|
||||
res["hx-vals"] = html.escape(json.dumps(res["hx-vals"]))
|
||||
|
||||
return res
|
||||
|
||||
def execute(self, client_response: dict = None):
|
||||
logger.debug(f"Executing command {self.name}")
|
||||
logger.debug(f"Executing command {self.name} with arguments {client_response=}")
|
||||
with ObservableResultCollector(self._bindings) as collector:
|
||||
kwargs = self._create_kwargs(self.default_kwargs,
|
||||
client_response,
|
||||
@@ -135,15 +140,18 @@ class Command:
|
||||
all_ret = flatten(ret, ret_from_bound_commands, collector.results)
|
||||
|
||||
# Set the hx-swap-oob attribute on all elements returned by the callback
|
||||
for r in all_ret[1:]:
|
||||
if (hasattr(r, 'attrs')
|
||||
and "hx-swap-oob" not in r.attrs
|
||||
and r.get("id", None) is not None):
|
||||
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
||||
if self._htmx_extra[AUTO_SWAP_OOB]:
|
||||
for r in all_ret[1:]:
|
||||
if (hasattr(r, 'attrs')
|
||||
and "hx-swap-oob" not in r.attrs
|
||||
and r.get("id", None) is not None):
|
||||
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
||||
|
||||
return all_ret[0] if len(all_ret) == 1 else all_ret
|
||||
|
||||
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None):
|
||||
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None, auto_swap_oob=True):
|
||||
self._htmx_extra[AUTO_SWAP_OOB] = auto_swap_oob
|
||||
|
||||
# Note that the default value is the same than in get_htmx_params()
|
||||
if target is None:
|
||||
self._htmx_extra["hx-swap"] = "none"
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from enum import Enum
|
||||
|
||||
DEFAULT_COLUMN_WIDTH = 100
|
||||
NO_DEFAULT_VALUE = object()
|
||||
|
||||
ROUTE_ROOT = "/myfasthtml"
|
||||
|
||||
# Datagrid
|
||||
ROW_INDEX_ID = "__row_index__"
|
||||
DATAGRID_DEFAULT_COLUMN_WIDTH = 100
|
||||
DATAGRID_PAGE_SIZE = 1000
|
||||
FILTER_INPUT_CID = "__filter_input__"
|
||||
|
||||
|
||||
class Routes:
|
||||
Commands = "/commands"
|
||||
Bindings = "/bindings"
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
from typing import Optional
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal
|
||||
|
||||
logger = logging.getLogger("InstancesManager")
|
||||
@@ -183,7 +184,7 @@ class InstancesManager:
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def get(session: dict, instance_id: str, default="**__no_default__**"):
|
||||
def get(session: dict, instance_id: str, default=NO_DEFAULT_VALUE):
|
||||
"""
|
||||
Get or create an instance of the given type (from its id)
|
||||
:param session:
|
||||
@@ -196,9 +197,9 @@ class InstancesManager:
|
||||
key = (session_id, instance_id)
|
||||
return InstancesManager.instances[key]
|
||||
except KeyError:
|
||||
if default != "**__non__**":
|
||||
return default
|
||||
raise
|
||||
if default is NO_DEFAULT_VALUE:
|
||||
raise
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def get_by_type(session: dict, cls: type):
|
||||
@@ -211,19 +212,19 @@ class InstancesManager:
|
||||
@staticmethod
|
||||
def dynamic_get(session, component_parent: tuple, component: tuple):
|
||||
component_type, component_id = component
|
||||
|
||||
|
||||
# 1. Check if component already exists
|
||||
existing = InstancesManager.get(session, component_id, None)
|
||||
if existing is not None:
|
||||
logger.debug(f"Component {component_id} already exists, returning existing instance")
|
||||
return existing
|
||||
|
||||
|
||||
# 2. Component doesn't exist, create it
|
||||
parent_type, parent_id = component_parent
|
||||
|
||||
|
||||
# parent should always exist
|
||||
parent = InstancesManager.get(session, parent_id)
|
||||
|
||||
|
||||
real_component_type = snake_to_pascal(component_type.removeprefix("mf-"))
|
||||
component_full_type = f"myfasthtml.controls.{real_component_type}.{real_component_type}"
|
||||
cls = get_class(component_full_type)
|
||||
|
||||
@@ -9,74 +9,84 @@ from functools import lru_cache
|
||||
|
||||
from fasthtml.common import NotStr
|
||||
|
||||
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||
|
||||
|
||||
class OptimizedFt:
|
||||
"""Lightweight FastHTML-compatible element that generates HTML directly."""
|
||||
|
||||
ATTR_MAP = {
|
||||
"cls": "class",
|
||||
"_id": "id",
|
||||
}
|
||||
|
||||
def __init__(self, tag, *args, **kwargs):
|
||||
self.tag = tag
|
||||
self.children = args
|
||||
self.attrs = {self.safe_attr(k): v for k, v in kwargs.items() if v is not None}
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=128)
|
||||
def safe_attr(attr_name):
|
||||
"""Convert Python attribute names to HTML attribute names."""
|
||||
attr_name = attr_name.replace("hx_", "hx-")
|
||||
attr_name = attr_name.replace("data_", "data-")
|
||||
return OptimizedFt.ATTR_MAP.get(attr_name, attr_name)
|
||||
|
||||
@staticmethod
|
||||
def to_html_helper(item):
|
||||
"""Convert any item to HTML string."""
|
||||
if item is None:
|
||||
return ""
|
||||
elif isinstance(item, str):
|
||||
return item
|
||||
elif isinstance(item, (int, float, bool)):
|
||||
return str(item)
|
||||
elif isinstance(item, OptimizedFt):
|
||||
return item.to_html()
|
||||
elif isinstance(item, NotStr):
|
||||
return str(item)
|
||||
else:
|
||||
raise Exception(f"Unsupported type: {type(item)}, {item=}")
|
||||
|
||||
def to_html(self):
|
||||
"""Generate HTML string."""
|
||||
# Build attributes
|
||||
attrs_list = []
|
||||
for k, v in self.attrs.items():
|
||||
if v is False:
|
||||
continue # Skip False attributes
|
||||
if v is True:
|
||||
attrs_list.append(k) # Boolean attribute
|
||||
else:
|
||||
# No need to escape v since we control the values (width, IDs, etc.)
|
||||
attrs_list.append(f'{k}="{v}"')
|
||||
|
||||
attrs_str = ' ' + ' '.join(attrs_list) if attrs_list else ''
|
||||
|
||||
# Build children HTML
|
||||
children_html = ''.join(self.to_html_helper(child) for child in self.children)
|
||||
|
||||
return f'<{self.tag}{attrs_str}>{children_html}</{self.tag}>'
|
||||
|
||||
def __ft__(self):
|
||||
"""FastHTML compatibility - returns NotStr to avoid double escaping."""
|
||||
return NotStr(self.to_html())
|
||||
|
||||
def __str__(self):
|
||||
return self.to_html()
|
||||
"""Lightweight FastHTML-compatible element that generates HTML directly."""
|
||||
|
||||
ATTR_MAP = {
|
||||
"cls": "class",
|
||||
"_id": "id",
|
||||
}
|
||||
|
||||
def __init__(self, tag, *args, **kwargs):
|
||||
self.tag = tag
|
||||
self.children = args
|
||||
self.attrs = {self.safe_attr(k): v for k, v in kwargs.items() if v is not None}
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=128)
|
||||
def safe_attr(attr_name):
|
||||
"""Convert Python attribute names to HTML attribute names."""
|
||||
attr_name = attr_name.replace("hx_", "hx-")
|
||||
attr_name = attr_name.replace("data_", "data-")
|
||||
return OptimizedFt.ATTR_MAP.get(attr_name, attr_name)
|
||||
|
||||
@staticmethod
|
||||
def to_html_helper(item):
|
||||
"""Convert any item to HTML string."""
|
||||
if item is None:
|
||||
return ""
|
||||
elif isinstance(item, str):
|
||||
return item
|
||||
elif isinstance(item, (int, float, bool)):
|
||||
return str(item)
|
||||
elif isinstance(item, OptimizedFt):
|
||||
return item.to_html()
|
||||
elif isinstance(item, NotStr):
|
||||
return str(item)
|
||||
else:
|
||||
raise Exception(f"Unsupported type: {type(item)}, {item=}")
|
||||
|
||||
def to_html(self):
|
||||
"""Generate HTML string."""
|
||||
# Build attributes
|
||||
attrs_list = []
|
||||
for k, v in self.attrs.items():
|
||||
if v is False:
|
||||
continue # Skip False attributes
|
||||
if v is True:
|
||||
attrs_list.append(k) # Boolean attribute
|
||||
else:
|
||||
# No need to escape v since we control the values (width, IDs, etc.)
|
||||
attrs_list.append(f'{k}="{v}"')
|
||||
|
||||
attrs_str = ' ' + ' '.join(attrs_list) if attrs_list else ''
|
||||
|
||||
# Build children HTML
|
||||
children_html = ''.join(self.to_html_helper(child) for child in self.children)
|
||||
|
||||
return f'<{self.tag}{attrs_str}>{children_html}</{self.tag}>'
|
||||
|
||||
def __ft__(self):
|
||||
"""FastHTML compatibility - returns NotStr to avoid double escaping."""
|
||||
return NotStr(self.to_html())
|
||||
|
||||
def __str__(self):
|
||||
return self.to_html()
|
||||
|
||||
def get(self, attr_name, default=NO_DEFAULT_VALUE):
|
||||
try:
|
||||
return self.attrs[self.safe_attr(attr_name)]
|
||||
except KeyError:
|
||||
if default is NO_DEFAULT_VALUE:
|
||||
raise
|
||||
return default
|
||||
|
||||
|
||||
class OptimizedDiv(OptimizedFt):
|
||||
"""Optimized Div element."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("div", *args, **kwargs)
|
||||
"""Optimized Div element."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("div", *args, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user