-
QuestionEx-Justpy newbee here. I am looking for the best pattern to meet these requirements:
In the examples below, you would have two browser windows open, one with (eg) Bob selected (manually), the other with Carol. When you go to /new_data in a third window and click 'Add row', the users should see Dave appear in the list, but the previously made selection should still be active. Code below is heavily based on the excellent example in #774, which also taught me how to control 'selected' rows programmatically. Thanks! So far, I have tried two (similar) approaches: # table_ui_state_with_refresh.py
from nicegui import ui
columns = [
{"name": "name", "label": "Name", "field": "name"},
{"name": "age", "label": "Age", "field": "age"},
]
rows = [{"name": "Alice", "age": 20}, {"name": "Bob", "age": 30}, {"name": "Carol", "age": 40}]
# registry of refreshable table functions
# TBD: client-id as key + remove disconnecting clients
refreshables = []
# when data comes in, call refresh on all registered table functions
def incoming_data(record: dict) -> None:
rows.append(record)
for r in refreshables:
r.refresh()
@ui.page("/")
async def main():
ui_state = {"selected_row": None}
@ui.refreshable
def mytable() -> None:
def refresh_selection(table) -> None:
table.selected.clear()
selected_row = ui_state["selected_row"]
if selected_row is not None:
table.selected.append(table.rows[selected_row])
table.update()
def on_select(rowIndex: int) -> None:
ui_state["selected_row"] = rowIndex
refresh_selection(table)
table = ui.table(columns=columns, rows=rows, row_key="name")
table.add_slot(
"body-cell",
r"""
<q-td :props="props" @click="$parent.$emit('cell_click', props)">
{{ props.value }}
</q-td>
""",
)
table.on("cell_click", lambda msg: on_select(msg["args"]["rowIndex"]))
refresh_selection(table)
# register the table function for refresh from global scope
refreshables.append(mytable)
mytable()
# simulates the out-of-band data coming in...
@ui.page("/new_data")
async def new_data():
ui.button("Add row", on_click=lambda e: incoming_data({"name": "Dave", "age": 50}))
ui.run(show=False) and b. add the ui state as a member to the table objects -> register these tables -> call update on them: # table_ui_state_with_update.py
from nicegui import ui
columns = [
{"name": "name", "label": "Name", "field": "name"},
{"name": "age", "label": "Age", "field": "age"},
]
rows = [{"name": "Alice", "age": 20}, {"name": "Bob", "age": 30}, {"name": "Carol", "age": 40}]
# registry of updateable tables
# TBD: client-id as key + remove disconnecting clients
tables = []
# ui_state is now a table instance member, making it accessible through the registry
def update_table_with_ui_state(table):
table.selected.clear()
selected_row = table.ui_state["selected_row"]
if selected_row is not None:
table.selected.append(table.rows[selected_row])
table.update()
# when data comes in, update table with data & ui_state and call .update()
def incoming_data(record: dict) -> None:
rows.append(record)
for t in tables:
update_table_with_ui_state(t)
@ui.page("/")
async def main():
def mytable() -> None:
def on_select(rowIndex: int) -> None:
table.ui_state["selected_row"] = rowIndex
update_table_with_ui_state(table)
table = ui.table(columns=columns, rows=rows, row_key="name")
table.add_slot(
"body-cell",
r"""
<q-td :props="props" @click="$parent.$emit('cell_click', props)">
{{ props.value }}
</q-td>
""",
)
table.on("cell_click", lambda msg: on_select(msg["args"]["rowIndex"]))
# ui_state is added as a instance member...
table.ui_state = {"selected_row": None}
# register the table for callbacks from global scope
tables.append(table)
mytable()
@ui.page("/new_data")
async def new_data():
ui.button("Add row", on_click=lambda e: incoming_data({"name": "Dave", "age": 50}))
ui.run(show=False) Both approaches require a registry that I fear will also have to be indexed by client-ID, in order to remove disconnecting clients. I have two questions:
Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 2 replies
-
The trick is to move the columns = [
{"name": "name", "label": "Name", "field": "name"},
{"name": "age", "label": "Age", "field": "age"},
]
rows = [{"name": "Alice", "age": 20}, {"name": "Bob", "age": 30}, {"name": "Carol", "age": 40}]
# when data comes in, call refresh on all registered table functions
def incoming_data(record: dict) -> None:
rows.append(record)
mytable.refresh()
@ui.refreshable
def mytable(ui_state: dict) -> None:
def refresh_selection(table) -> None:
table.selected.clear()
selected_row = ui_state["selected_row"]
if selected_row is not None:
table.selected.append(table.rows[selected_row])
table.update()
def on_select(rowIndex: int) -> None:
ui_state["selected_row"] = rowIndex
refresh_selection(table)
table = ui.table(columns=columns, rows=rows, row_key="name")
table.add_slot(
"body-cell",
r"""
<q-td :props="props" @click="$parent.$emit('cell_click', props)">
{{ props.value }}
</q-td>
""",
)
table.on("cell_click", lambda msg: on_select(msg["args"]["rowIndex"]))
refresh_selection(table)
@ui.page("/")
async def main():
ui_state = {"selected_row": None}
mytable(ui_state)
# simulates the out-of-band data coming in...
@ui.page("/new_data")
async def new_data():
ui.button("Add row", on_click=lambda e: incoming_data({"name": "Dave", "age": 50})) |
Beta Was this translation helpful? Give feedback.
-
Thanks, Rodja, I was thinking "it has to be simpler than this" - and I am happy to see that it is. ;-) |
Beta Was this translation helpful? Give feedback.
-
This is a really interesting problem, @kleynjan! And the solution is not trivial. But @rodja's approach is working nicely. I played around with it and came up with the following simplified example, focussing on the refresh mechanics by using simple labels instead of a rather complex table. It's not meant to replace your code, but might help someone stumbling across this discussion to grasp the idea: names = ['Alice', 'Bob', 'Charlie']
def handle_new_name(name: str) -> None:
names.append(name)
name_list.refresh()
@ui.refreshable
def name_list(ui_state: dict) -> None:
for name in names:
ui.label(name.upper() if ui_state['uppercase'] else name.lower())
@ui.page('/')
def main():
ui_state = {'uppercase': False}
name_list(ui_state)
ui.switch('Uppercase', on_change=name_list.refresh).bind_value_to(ui_state, 'uppercase')
@ui.page('/new_name')
def new_name():
name = ui.input('New name')
ui.button('Add name', on_click=lambda: handle_new_name(name.value)) |
Beta Was this translation helpful? Give feedback.
-
Thanks, Falko. Minimal examples are always best. Ultimately, I think this is about predictable reactivity. Using a 'store' as a single source of truth, routing all changes through this store and then knowing which components need to update or refresh. Eg, Pinia. It would be interesting to link the existing binding mechanisms in Nicegui to such a store (one-way, in that case, I think) - but for now, that is way above my 'pay grade' and I can do what I need to do with the examples provided here. I really really like what you all have been doing with Nicegui in just a few months, btw! |
Beta Was this translation helpful? Give feedback.
The trick is to move the
refreshable
out of the page creator function as shown in our chat app example. Therefreshable
tracks every registered client and updates accordingly: