Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

data table tutorial #304

Merged
merged 14 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions docs/datatable-tutorial/add_interactivity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
```python exec
import reflex as rx
from pcweb.base_state import State
from pcweb.templates.docpage import docdemobox
from pcweb.pages.docs.datatable_tutorial.states import DataTableState, DataTableState2

```





# Adding Interactivity to our DataTable


Now we will add interactivity to our datatable. We do this using event handlers and event triggers.


The first example implements a handler for the `on_cell_clicked` event trigger, which is called when the user clicks on a cell of the data editor. The event trigger receives the coordinates of the cell.

```python
class DataTableState(rx.State):
clicked_cell: str = "Cell clicked: "

...


def get_clicked_data(self, pos: tuple[int, int]) -> str:
self.clicked_cell = f"Cell clicked: \{pos}"

```

The state has a var called `clicked_cell` that will store a message about which cell was clicked. We define an event handler `get_clicked_data` that updates the value of the `clicked_cell` var when it is called. In essence, we have clicked on a cell, called the `on_cell_clicked` event trigger which calls the `get_clicked_data` event handler, which updates the `clicked_cell` var.


```python demo
rx.text(DataTableState.clicked_cell, as_="strong")
```

```python demo
rx.data_editor(
columns=DataTableState.cols,
data=DataTableState.data,
on_cell_clicked=DataTableState.get_clicked_data,
)
```


The event handler `on_cell_context_menu` can be used in the same way as `on_cell_clicked`, except here the event trigger is called when the user right clicks, i.e. when the cell should show a context menu.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any example anywhere of displaying a custom context menu? I feel like this is a bit of a tease if we're not going to show the code. And it's a bit more complicated than it sounds here.

At the end of the day, we might leave this line out, or save it for later in the tutorial/API reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We give an example of on_group_header_context_menu later on. These are not really custom context menus, it is just an event handler that picks up a right mouse click


## Editing cells

Another important type of interactivity we will showcase is how to edit cells. Here we use the `on_cell_edited` event trigger to update the data based on what the user entered.

```python
class DataTableState(rx.State):
clicked_cell: str = "Cell clicked: "
edited_cell: str = "Cell edited: "

...


def get_clicked_data(self, pos) -> str:
self.clicked_cell = f"Cell clicked: \{pos}"

def get_edited_data(self, pos, val) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest more type annotations here.

What is the type of val? Is it based on the column type for the table? Is it always a str and I have to cast it myself?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val is a dictionary like this: {'kind': 'text', 'data': '20', 'displayData': 'Hermione Jean Granger', 'readonly': False, 'allowOverlay': True} and we index in and just use the 'data' key for now

col, row = pos
self.data[row][col] = val["data"]
self.edited_cell = f"Cell edited: \{pos}, Cell value: \{val["data"]}"

```

The `on_cell_edited` event trigger is called when the user modifies the content of a cell. It receives the coordinates of the cell and the modified content. We pass these into the `get_edited_data` event handler and use them to update the `data` state var at the appropriate position. We then update the `edited_cell` var value.


```python demo
rx.text(DataTableState.edited_cell, as_="strong")
```

```python demo
rx.data_editor(
columns=DataTableState.cols,
data=DataTableState.data,
on_cell_clicked=DataTableState.get_clicked_data,
on_cell_edited=DataTableState.get_edited_data,
)
```

## Group Header

We can define group headers which are headers that encompass a group of columns. We define these in the `columns` using the `group` property such as `"group": "Data"`. The `columns` would now be defined as below. Only the `Title` does not fall under a group header, all the rest fall under the `Data` group header.

```python
class DataTableState2(rx.State):
"""The app state."""

...

cols: list[dict] = [
{\"title": "Title", "type": "str"},
{
"title": "Name",
"type": "str",
"group": "Data",
"width": 300,
},
{
"title": "Birth",
"type": "str",
"group": "Data",
"width": 150,
},
{
"title": "Human",
"type": "bool",
"group": "Data",
"width": 80,
},
{
"title": "House",
"type": "str",
"group": "Data",
},
{
"title": "Wand",
"type": "str",
"group": "Data",
"width": 250,
},
{
"title": "Patronus",
"type": "str",
"group": "Data",
},
{
"title": "Blood status",
"type": "str",
"group": "Data",
"width": 200,
},
]

...
```


The table now has a header as below.

```python demo
rx.data_editor(
columns=DataTableState2.cols,
data=DataTableState2.data,
on_cell_clicked=DataTableState2.get_clicked_data,
on_cell_edited=DataTableState2.get_edited_data,
)
```

There are several event triggers we can apply to the group header.


```python
class DataTableState2(rx.State):
"""The app state."""

right_clicked_group_header : str = "Group header right clicked: "

...

def get_group_header_right_click(self, index, val):
self.right_clicked_group_header = f"Group header right clicked at index: \{index}, Group header value: \{val['group']}"
Comment on lines +169 to +170
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i see the utility in this example. it works, but i don't know why someone would want this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is just an example that shows how to do a right click event handler. I assume people will figure out their own uses for it if they want to ever create events based on right clicks from the user


```

```python demo
rx.text(DataTableState2.right_clicked_group_header, as_="strong")
```

```python demo
rx.data_editor(
columns=DataTableState2.cols,
data=DataTableState2.data,
on_cell_clicked=DataTableState2.get_clicked_data,
on_cell_edited=DataTableState2.get_edited_data,
on_group_header_context_menu=DataTableState2.get_group_header_right_click,
)
```

In this example we use the `on_group_header_context_menu` event trigger which is called when the user right-clicks on a group header. It returns the `index` and the `data` of the group header. We can also use the `on_group_header_clicked` and `on_group_header_renamed` event triggers which are called when the user left-clicks on a group header and when a user renames a group header respectively.


## More Event Triggers

There are several other event triggers that are worth exploring. The `on_item_hovered` event trigger is called whenever the user hovers over an item in the datatable. The `on_delete` event trigger is called when the user deletes a cell from the datatable.
tgberkeley marked this conversation as resolved.
Show resolved Hide resolved

The final event trigger to check out is `on_column_resize`. `on_column_resize` allows us to respond to the user dragging the handle between columns. The event trigger returns the `col` we are adjusting and the new `width` we have defined. The `col` that is returned is a dictionary for example: `\{'title': 'Name', 'type': 'str', 'group': 'Data', 'width': 198, 'pos': 1}`. We then index into `self.cols` defined in our state and change the `width` of that column using this code: `self.cols[col['pos']]['width'] = width`.


```python
class DataTableState2(rx.State):
"""The app state."""

...

item_hovered: str = "Item Hovered: "
deleted: str = "Deleted: "

...


def get_item_hovered(self, pos) -> str:
self.item_hovered = f"Item Hovered type: \{pos['kind']}, Location: \{pos['location']}"

def get_deleted_item(self, selection):
self.deleted = f"Deleted cell: \{selection['current']['cell']}"

def column_resize(self, col, width):
self.cols[col['pos']]['width'] = width
```


```python demo
rx.text(DataTableState2.item_hovered, as_="strong")
```

```python demo
rx.text(DataTableState2.deleted, as_="strong")
```

```python demo
rx.data_editor(
columns=DataTableState2.cols,
data=DataTableState2.data,
on_cell_clicked=DataTableState2.get_clicked_data,
on_cell_edited=DataTableState2.get_edited_data,
on_group_header_context_menu=DataTableState2.get_group_header_right_click,
on_item_hovered=DataTableState2.get_item_hovered,
on_delete=DataTableState2.get_deleted_item,
on_column_resize=DataTableState2.column_resize,
)
```
144 changes: 144 additions & 0 deletions docs/datatable-tutorial/add_styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
```python exec
import reflex as rx
from pcweb.pages.docs.datatable_tutorial.states import DataTableState2
```



# DataTable Styling


There are props that we can explore to ensure the datatable is shaped correctly and reacts in the way we expect. We can set `on_paste` to `True`, which allows us to paste directly into a cell. We can use `draw_focus_ring` to draw a ring around the cells when selected, this defaults to `True` so can be turned off if we do not want it. The `rows` prop can be used to hard code the number of rows that we show.

`freeze_columns` is used to keep a certain number of the left hand columns frozen when scrolling horizontally. `group_header_height` and `header_height` define the height of the group header and the individual headers respectively. `max_column_width` and `min_column_width` define how large or small the columns are allowed to be with the manual column resizing. We can also define the `row_height` to make the rows more nicely spaced.

We can add `row_markers`, which appear on the furthest left side of the table. They can take values of `'none', 'number', 'checkbox', 'both', 'clickable-number'`. We can set `smooth_scroll_x` and `smooth_scroll_y`, which allows us to smoothly scroll along the columns and rows.

By default there is a `vertical_border` between the columns, we can turn it off by setting this prop to `False`. We can define how many columns a user can select at a time by setting the `column_select` prop. It can take values of `"none", "single", "multi"`.

We can allow `overscroll_x`, which allows users to scroll past the limit of the actual horizontal content. There is an equivalent `overscroll_y`.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest directly linking to the component docs where all of these props (and more) are displayed in table form.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Check out [these docs](https://reflex.dev/docs/library/datadisplay/dataeditor/) for more information on these props.

```python demo
rx.data_editor(
columns=DataTableState2.cols,
data=DataTableState2.data,

#rows=4,
on_paste=True,
draw_focus_ring=False,
freeze_columns=2,
group_header_height=50,
header_height=60,
max_column_width=300,
min_column_width=100,
row_height=50,
row_markers='clickable-number',
smooth_scroll_x=True,
vertical_border=False,
column_select="multi",
overscroll_x=100,


on_cell_clicked=DataTableState2.get_clicked_data,
on_cell_edited=DataTableState2.get_edited_data,
on_group_header_context_menu=DataTableState2.get_group_header_right_click,
on_item_hovered=DataTableState2.get_item_hovered,
on_delete=DataTableState2.get_deleted_item,
on_column_resize=DataTableState2.column_resize,
)
```


## Theming

Lastly there is a `theme` prop that allows us to pass in all color and font information for the data table.

```python
darkTheme = {
"accentColor": "#8c96ff",
"accentLight": "rgba(202, 206, 255, 0.253)",
"textDark": "#ffffff",
"textMedium": "#b8b8b8",
"textLight": "#a0a0a0",
"textBubble": "#ffffff",
"bgIconHeader": "#b8b8b8",
"fgIconHeader": "#000000",
"textHeader": "#a1a1a1",
"textHeaderSelected": "#000000",
"bgCell": "#16161b",
"bgCellMedium": "#202027",
"bgHeader": "#212121",
"bgHeaderHasFocus": "#474747",
"bgHeaderHovered": "#404040",
"bgBubble": "#212121",
"bgBubbleSelected": "#000000",
"bgSearchResult": "#423c24",
"borderColor": "rgba(225,225,225,0.2)",
"drilldownBorder": "rgba(225,225,225,0.4)",
"linkColor": "#4F5DFF",
"headerFontStyle": "bold 14px",
"baseFontStyle": "13px",
"fontFamily": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif",
}
```

```python exec
darkTheme = {
"accentColor": "#8c96ff",
"accentLight": "rgba(202, 206, 255, 0.253)",
"textDark": "#ffffff",
"textMedium": "#b8b8b8",
"textLight": "#a0a0a0",
"textBubble": "#ffffff",
"bgIconHeader": "#b8b8b8",
"fgIconHeader": "#000000",
"textHeader": "#a1a1a1",
"textHeaderSelected": "#000000",
"bgCell": "#16161b",
"bgCellMedium": "#202027",
"bgHeader": "#212121",
"bgHeaderHasFocus": "#474747",
"bgHeaderHovered": "#404040",
"bgBubble": "#212121",
"bgBubbleSelected": "#000000",
"bgSearchResult": "#423c24",
"borderColor": "rgba(225,225,225,0.2)",
"drilldownBorder": "rgba(225,225,225,0.4)",
"linkColor": "#4F5DFF",
"headerFontStyle": "bold 14px",
"baseFontStyle": "13px",
"fontFamily": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif",
}
```

```python demo
rx.data_editor(
columns=DataTableState2.cols,
data=DataTableState2.data,

on_paste=True,
draw_focus_ring=False,
freeze_columns=2,
group_header_height=50,
header_height=60,
max_column_width=300,
min_column_width=100,
row_height=50,
row_markers='clickable-number',
smooth_scroll_x=True,
vertical_border=False,
column_select="multi",
overscroll_x=100,
theme=darkTheme,


on_cell_clicked=DataTableState2.get_clicked_data,
on_cell_edited=DataTableState2.get_edited_data,
on_group_header_context_menu=DataTableState2.get_group_header_right_click,
on_item_hovered=DataTableState2.get_item_hovered,
on_delete=DataTableState2.get_deleted_item,
on_column_resize=DataTableState2.column_resize,
)
```
Loading