diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css
index 6f515eb..3b54707 100644
--- a/src/myfasthtml/assets/myfasthtml.css
+++ b/src/myfasthtml/assets/myfasthtml.css
@@ -965,7 +965,7 @@ input:focus {
flex-direction: column;
border: 1px solid var(--color-border);
border-radius: 10px;
- overflow: hidden;
+ /* overflow: hidden; */
}
.dt2-table:focus {
diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py
index c33ed90..6dcc053 100644
--- a/src/myfasthtml/controls/DataGrid.py
+++ b/src/myfasthtml/controls/DataGrid.py
@@ -11,6 +11,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.controls.helpers import mk
+from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
@@ -25,14 +26,14 @@ _HTML_SPECIAL_CHARS_REGEX = re.compile(r'[<>&"\']')
@lru_cache(maxsize=2)
def _mk_bool_cached(_value):
- """
- OPTIMIZED: Cached boolean checkbox HTML generator.
- Since there are only 2 possible values (True/False), this will only generate HTML twice.
- """
- return NotStr(str(
- Div(mk.icon(checkbox_checked16_regular if _value else checkbox_unchecked16_regular, can_select=False),
- cls="dt2-cell-content-checkbox")
- ))
+ """
+ OPTIMIZED: Cached boolean checkbox HTML generator.
+ Since there are only 2 possible values (True/False), this will only generate HTML twice.
+ """
+ return NotStr(str(
+ Div(mk.icon(checkbox_checked16_regular if _value else checkbox_unchecked16_regular, can_select=False),
+ cls="dt2-cell-content-checkbox")
+ ))
class DatagridState(DbObject):
@@ -41,7 +42,7 @@ class DatagridState(DbObject):
with self.initializing():
self.sidebar_visible: bool = False
self.selected_view: str = None
- self.row_index: bool = False
+ self.row_index: bool = True
self.columns: list[DataGridColumnState] = []
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
self.headers: list[DataGridHeaderFooterConf] = []
@@ -70,7 +71,17 @@ class DatagridSettings(DbObject):
class Commands(BaseCommands):
- pass
+ def get_page(self, page_index: int):
+ return Command("GetPage",
+ "Get a specific page of data",
+ self._owner,
+ self._owner.mk_body_content_page,
+ kwargs={"page_index": page_index}
+ ).htmx(target=f"#tb_{self._id}",
+ swap="beforeend",
+ trigger=f"intersect root:#tb_{self._id} once",
+ auto_swap_oob=False
+ )
class DataGrid(MultipleInstance):
@@ -175,18 +186,18 @@ class DataGrid(MultipleInstance):
- Avoids html.escape when not necessary
- Uses cached boolean HTML (_mk_bool_cached)
"""
-
+
def mk_highlighted_text(value_str, css_class):
"""Return highlighted text as raw HTML string or tuple of Spans."""
if not filter_keyword_lower:
# OPTIMIZATION: Return plain HTML string instead of Label object
# Include "truncate text-sm" to match mk.label() behavior (ellipsis + font size)
return NotStr(f'{value_str}')
-
+
index = value_str.lower().find(filter_keyword_lower)
if index < 0:
return NotStr(f'{value_str}')
-
+
# Has highlighting - need to use Span objects
# Add "truncate text-sm" to match mk.label() behavior
len_keyword = len(filter_keyword_lower)
@@ -197,25 +208,25 @@ class DataGrid(MultipleInstance):
if index + len_keyword < len(value_str):
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class} text-sm"))
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
-
+
column_type = col_def.type
value = self._state.ns_fast_access[col_def.col_id][row_index]
-
+
# Boolean type - uses cached HTML (only 2 possible values)
if column_type == ColumnType.Bool:
return _mk_bool_cached(value)
-
+
# RowIndex - simplest case, just return the number as plain HTML
if column_type == ColumnType.RowIndex:
return NotStr(f'{row_index}')
-
+
# Convert value to string
value_str = str(value)
-
+
# OPTIMIZATION: Only escape if necessary (check for HTML special chars with pre-compiled regex)
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
value_str = html.escape(value_str)
-
+
# Number or Text type
if column_type == ColumnType.Number:
return mk_highlighted_text(value_str, "dt2-cell-content-number")
@@ -229,12 +240,12 @@ class DataGrid(MultipleInstance):
"""
if not col_def.usable:
return None
-
+
if not col_def.visible:
return OptimizedDiv(cls="dt2-col-hidden")
-
+
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
-
+
return OptimizedDiv(content,
data_col=col_def.col_id,
style=f"width:{col_def.width}px;",
@@ -252,19 +263,19 @@ class DataGrid(MultipleInstance):
last_row = df.index[end - 1]
else:
last_row = None
-
- # OPTIMIZATION: Extract filter keyword once (was being checked 10,000 times)
+
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
-
+
rows = [OptimizedDiv(
*[self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower)
for col_pos, col_def in enumerate(self._state.columns)],
cls="dt2-row",
data_row=f"{row_index}",
- _id=f"tr_{self._id}-{row_index}",
+ id=f"tr_{self._id}-{row_index}",
+ **self.commands.get_page(page_index + 1).get_htmx_params(escaped=True) if row_index == last_row else {}
) for row_index in df.index[start:end]]
-
+
return rows
def mk_body(self):
@@ -290,7 +301,9 @@ class DataGrid(MultipleInstance):
return Div(
self.mk_headers(),
self.mk_body(),
- self.mk_footers()
+ self.mk_footers(),
+ cls="dt2-table",
+ id=f"t_{self._id}",
)
def mk_aggregation_cell(self, col_def, row_index: int, footer_conf, oob=False):
diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py
index b444f89..e5f62d8 100644
--- a/src/myfasthtml/controls/DataGridsManager.py
+++ b/src/myfasthtml/controls/DataGridsManager.py
@@ -1,5 +1,6 @@
import uuid
from dataclasses import dataclass
+from io import BytesIO
import pandas as pd
from fasthtml.components import Div
@@ -90,7 +91,7 @@ class DataGridsManager(MultipleInstance):
def open_from_excel(self, tab_id, file_upload: FileUpload):
excel_content = file_upload.get_content()
- df = pd.read_excel(excel_content, file_upload.get_sheet_name())
+ df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
dg = DataGrid(self._tabs_manager, save_state=True)
dg.init_from_dataframe(df)
document = DocumentDefinition(
diff --git a/src/myfasthtml/controls/datagrid_objects.py b/src/myfasthtml/controls/datagrid_objects.py
index 277bfa8..368e2bf 100644
--- a/src/myfasthtml/controls/datagrid_objects.py
+++ b/src/myfasthtml/controls/datagrid_objects.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass, field
-from myfasthtml.core.constants import ColumnType, DEFAULT_COLUMN_WIDTH, ViewType
+from myfasthtml.core.constants import ColumnType, DATAGRID_DEFAULT_COLUMN_WIDTH, ViewType
@dataclass
@@ -17,7 +17,7 @@ class DataGridColumnState:
type: ColumnType = ColumnType.Text
visible: bool = True
usable: bool = True
- width: int = DEFAULT_COLUMN_WIDTH
+ width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
@dataclass
diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py
index ff07149..be0daef 100644
--- a/src/myfasthtml/core/commands.py
+++ b/src/myfasthtml/core/commands.py
@@ -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"
diff --git a/src/myfasthtml/core/constants.py b/src/myfasthtml/core/constants.py
index f87b623..be32113 100644
--- a/src/myfasthtml/core/constants.py
+++ b/src/myfasthtml/core/constants.py
@@ -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"
diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py
index ec8f766..760de6a 100644
--- a/src/myfasthtml/core/instances.py
+++ b/src/myfasthtml/core/instances.py
@@ -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)
diff --git a/src/myfasthtml/core/optimized_ft.py b/src/myfasthtml/core/optimized_ft.py
index 087f9fe..58142ef 100644
--- a/src/myfasthtml/core/optimized_ft.py
+++ b/src/myfasthtml/core/optimized_ft.py
@@ -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)