Skip to content

Commit afc916a

Browse files
committed
fix(datatable): ensure columns work with DataFrame, list, and Var inputs
1 parent c3a7294 commit afc916a

File tree

4 files changed

+183
-56
lines changed

4 files changed

+183
-56
lines changed

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42"
2+
"reflex/components/gridjs/datatable.pyi": "e1f34ade3873a931770da4a35586f298"
33
}
Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Table components."""
1+
"""Table components for Reflex using Gridjs."""
22

33
from __future__ import annotations
44

@@ -14,58 +14,52 @@
1414

1515

1616
class Gridjs(NoSSRComponent):
17-
"""A component that wraps a nivo bar component."""
17+
"""A base component that wraps Gridjs (JS library) for tables."""
1818

1919
library = "gridjs-react@6.1.1"
20-
2120
lib_dependencies: list[str] = ["gridjs@6.2.0"]
2221

2322

2423
class DataTable(Gridjs):
25-
"""A data table component."""
24+
"""A flexible data table component for Reflex.
2625
27-
tag = "Grid"
26+
Supports:
27+
- Pandas DataFrames
28+
- Python lists
29+
- Reflex Vars (state variables)
30+
"""
2831

32+
tag = "Grid"
2933
alias = "DataTableGrid"
3034

31-
# The data to display. Either a list of lists or a pandas dataframe.
32-
data: Any
33-
34-
# The list of columns to display. Required if data is a list and should not be provided
35-
# if the data field is a dataframe
36-
columns: Var[Sequence]
37-
38-
# Enable a search bar.
39-
search: Var[bool]
40-
41-
# Enable sorting on columns.
42-
sort: Var[bool]
43-
44-
# Enable resizable columns.
45-
resizable: Var[bool]
46-
47-
# Enable pagination.
48-
pagination: Var[bool | dict]
49-
35+
# -----------------------------
36+
# Component Props
37+
# -----------------------------
38+
data: Any # The data to display (list of lists or DataFrame)
39+
columns: Var[Sequence] # Columns to display (optional if using DataFrame)
40+
search: Var[bool] # Enable search
41+
sort: Var[bool] # Enable sorting
42+
resizable: Var[bool] # Enable column resizing
43+
pagination: Var[bool | dict] # Enable pagination
44+
45+
# -----------------------------
46+
# Component creation
47+
# -----------------------------
5048
@classmethod
5149
def create(cls, *children, **props):
52-
"""Create a datatable component.
50+
"""Create a DataTable component with proper validation.
5351
54-
Args:
55-
*children: The children of the component.
56-
**props: The props to pass to the component.
52+
Raises:
53+
ValueError: If both DataFrame and columns are provided, or
54+
if columns are missing for a list-type data field.
5755
5856
Returns:
59-
The datatable component.
60-
61-
Raises:
62-
ValueError: If a pandas dataframe is passed in and columns are also provided.
57+
DataTable: The created DataTable component.
6358
"""
6459
data = props.get("data")
6560
columns = props.get("columns")
6661

67-
# The annotation should be provided if data is a computed var. We need this to know how to
68-
# render pandas dataframes.
62+
# 1️⃣ Ensure computed Vars have type annotations
6963
if is_computed_var(data) and data._var_type == Any:
7064
msg = "Annotation of the computed var assigned to the data field should be provided."
7165
raise ValueError(msg)
@@ -78,38 +72,50 @@ def create(cls, *children, **props):
7872
msg = "Annotation of the computed var assigned to the column field should be provided."
7973
raise ValueError(msg)
8074

81-
# If data is a pandas dataframe and columns are provided throw an error.
75+
# 2️⃣ Disallow DataFrame + columns (columns auto-detected from DataFrame)
8276
if (
8377
types.is_dataframe(type(data))
8478
or (isinstance(data, Var) and types.is_dataframe(data._var_type))
8579
) and columns is not None:
8680
msg = "Cannot pass in both a pandas dataframe and columns to the data_table component."
8781
raise ValueError(msg)
8882

89-
# If data is a list and columns are not provided, throw an error
83+
# 3️⃣ Require columns if data is a list
9084
if (
9185
(isinstance(data, Var) and types.typehint_issubclass(data._var_type, list))
9286
or isinstance(data, list)
9387
) and columns is None:
94-
msg = "column field should be specified when the data field is a list type"
88+
msg = "Column field should be specified when the data field is a list type"
9589
raise ValueError(msg)
9690

97-
# Create the component.
98-
return super().create(
99-
*children,
100-
**props,
101-
)
91+
# 4️⃣ Call parent create method
92+
return super().create(*children, **props)
10293

94+
# -----------------------------
95+
# Add external imports (CSS)
96+
# -----------------------------
10397
def add_imports(self) -> ImportDict:
104-
"""Add the imports for the datatable component.
98+
"""Add CSS for Gridjs.
10599
106100
Returns:
107-
The import dict for the component.
101+
ImportDict: The import dictionary required for the component.
108102
"""
109103
return {"": "gridjs/dist/theme/mermaid.css"}
110104

105+
# -----------------------------
106+
# Render component
107+
# -----------------------------
111108
def _render(self) -> Tag:
109+
"""Normalize columns and prepare data for front-end rendering.
110+
111+
Returns:
112+
Tag: The rendered table component.
113+
"""
114+
# -----------------------------
115+
# Case 1: DataFrame coming from State (Var)
116+
# -----------------------------
112117
if isinstance(self.data, Var) and types.is_dataframe(self.data._var_type):
118+
# Convert DataFrame to front-end-safe Vars
113119
self.columns = self.data._replace(
114120
_js_expr=f"{self.data._js_expr}.columns",
115121
_var_type=list[Any],
@@ -118,23 +124,53 @@ def _render(self) -> Tag:
118124
_js_expr=f"{self.data._js_expr}.data",
119125
_var_type=list[list[Any]],
120126
)
127+
128+
# -----------------------------
129+
# Case 2: DataFrame passed directly from Python
130+
# -----------------------------
121131
if types.is_dataframe(type(self.data)):
122-
# If given a pandas df break up the data and columns
123132
data = serialize(self.data)
124133
if not isinstance(data, dict):
125134
msg = "Serialized dataframe should be a dict."
126135
raise ValueError(msg)
136+
137+
# Convert Python lists to LiteralVars for front-end rendering
127138
self.columns = LiteralVar.create(data["columns"])
128139
self.data = LiteralVar.create(data["data"])
129140

130-
# If columns is a list of strings convert to list of dicts with id and name keys
131-
if isinstance(self.columns, LiteralVar) and isinstance(
132-
self.columns._var_value, list
133-
):
134-
self.columns = LiteralVar.create([
135-
{"id": col, "name": col} if isinstance(col, str) else col
136-
for col in self.columns._var_value
137-
])
138-
139-
# Render the table.
141+
# -----------------------------
142+
# Case 3: Normalize columns for all other scenarios
143+
# -----------------------------
144+
if self.columns is not None:
145+
# Python list → LiteralVar
146+
if isinstance(self.columns, list):
147+
self.columns = LiteralVar.create([
148+
{"id": col, "name": col} if isinstance(col, str) else col
149+
for col in self.columns
150+
])
151+
152+
# LiteralVar[list] → normalized LiteralVar
153+
elif isinstance(self.columns, LiteralVar) and isinstance(
154+
self.columns._var_value, list
155+
):
156+
self.columns = LiteralVar.create([
157+
{"id": col, "name": col} if isinstance(col, str) else col
158+
for col in self.columns._var_value
159+
])
160+
161+
# Var[list] → frontend-safe JS mapping (compile-time + runtime safe)
162+
elif isinstance(self.columns, Var):
163+
self.columns = self.columns._replace(
164+
_js_expr=(
165+
f"{self.columns._js_expr}.map("
166+
"(col) => typeof col === 'string' "
167+
"? ({ id: col, name: col }) "
168+
": col)"
169+
),
170+
_var_type=list[Any],
171+
)
172+
173+
# -----------------------------
174+
# Case 4: Render component
175+
# -----------------------------
140176
return super()._render()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""gridjs component tests."""
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import pandas as pd
2+
import pytest
3+
4+
from reflex.components.gridjs import DataTable
5+
from reflex.state import State
6+
from reflex.vars.base import LiteralVar, Var
7+
8+
9+
class TableState(State):
10+
"""TableState used by tests."""
11+
12+
df: pd.DataFrame = pd.DataFrame({"A": [1], "B": [2]})
13+
data: list[list[int]] = [[1, 2]]
14+
columns: list[str] = ["A", "B"]
15+
16+
17+
def test_dataframe_python_columns_normalized():
18+
"""DataFrame passed as a Python value should produce normalized LiteralVar columns."""
19+
df = pd.DataFrame({"A": [1], "B": [2]})
20+
21+
table = DataTable.create(data=df)
22+
table._render()
23+
24+
columns = table.columns # type: ignore[attr-defined]
25+
26+
assert isinstance(columns, LiteralVar)
27+
assert columns._var_value == [
28+
{"id": "A", "name": "A"},
29+
{"id": "B", "name": "B"},
30+
]
31+
32+
33+
def test_dataframe_var_columns_preserved():
34+
"""DataFrame coming from State is a runtime value.
35+
Columns must remain a Var (frontend expression),
36+
not be eagerly converted to LiteralVar.
37+
"""
38+
table = DataTable.create(data=TableState.df)
39+
table._render()
40+
41+
columns = table.columns # type: ignore[attr-defined]
42+
43+
assert isinstance(columns, Var)
44+
assert not isinstance(columns, LiteralVar)
45+
46+
47+
def test_list_columns_python_normalized():
48+
"""Python list of column names should be normalized eagerly."""
49+
table = DataTable.create(
50+
data=[[1, 2]],
51+
columns=["A", "B"],
52+
)
53+
table._render()
54+
55+
columns = table.columns # type: ignore[attr-defined]
56+
57+
assert isinstance(columns, LiteralVar)
58+
assert columns._var_value == [
59+
{"id": "A", "name": "A"},
60+
{"id": "B", "name": "B"},
61+
]
62+
63+
64+
def test_list_columns_var_preserved():
65+
"""Columns coming from State must remain a Var so they
66+
can be transformed on the frontend.
67+
"""
68+
table = DataTable.create(
69+
data=TableState.data,
70+
columns=TableState.columns,
71+
)
72+
table._render()
73+
74+
columns = table.columns # type: ignore[attr-defined]
75+
76+
assert isinstance(columns, Var)
77+
assert not isinstance(columns, LiteralVar)
78+
79+
80+
def test_dataframe_with_columns_raises():
81+
"""DataFrame already defines columns.
82+
Passing explicit columns is ambiguous and must error.
83+
"""
84+
df = pd.DataFrame({"A": [1]})
85+
86+
with pytest.raises(ValueError):
87+
DataTable.create(
88+
data=df,
89+
columns=["A"],
90+
)

0 commit comments

Comments
 (0)