zauberzeug / nicegui

Create web-based user interfaces with Python. The nice way.
https://nicegui.io
MIT License
9.18k stars 547 forks source link

AG Grids inside hidden tabs don't get mounted #3033

Open falkoschindler opened 5 months ago

falkoschindler commented 5 months ago

Description

As noticed in https://github.com/zauberzeug/nicegui/issues/1869#issuecomment-1851381149, when creating AG Grids on currently hidden tabs, they don't get mounted and interacting with them can cause problems:

with ui.tabs().classes('w-full') as tabs:
    one = ui.tab('One')
    two = ui.tab('Two')
with ui.tab_panels(tabs, value=one).classes('w-full'):
    with ui.tab_panel(one):
        async def get_data():
            ui.notify(await grid.get_client_data())
        ui.button('Get data', on_click=get_data)
    with ui.tab_panel(two):
        grid = ui.aggrid({
            'columnDefs': [{'headerName': 'Name', 'field': 'name'}],
            'rowData': [{'name': 'Alice'}, {'name': 'Bob'}],
        })

When clicking "Get data" before visiting tab "Two" for the first time, a JavaScript error occurs and we don't see a notification.

How can we make sure every NiceGUI element is created, even if it is hidden on another tab?


Related discussion: #2157

kyloe commented 4 months ago

I've done some searching in Quasar and Vue disciussions and have found these topics, https://github.com/quasarframework/quasar/discussions/16329 and https://github.com/quasarframework/quasar/discussions/15912.

Reading them suggests that rather than being a bug in NiceGUI it's actually a problem with Quasar. Ive looked at the solutions but they involve adding js in the Quasar environemnt - and I'm a bit new to NiceGUI, so struggling to see how NiceGUI can push these code fragments into the Quasar rendering of the nicegui app.

If anyone has a pointer - I will happily pursue the matter :)

falkoschindler commented 4 months ago

Interesting news, @kyloe!

So as far as I understand, they suggest to traverse the tabs once they are mounted. We could wrap the Quasar tab components in order to add such code to the mounted hook. But I managed to achieve something similar by adding code to TabPanel.__init__:

class TabPanel(DisableableElement):

    def __init__(self, name: Union[Tab, str]) -> None:
        # ...

        assert isinstance(context.slot.parent, TabPanels)
        panels: TabPanels = context.slot.parent
        current_tab = panels._props['model-value']
        panels.run_method('goTo', self._props['name'])
        panels.run_method('goTo', current_tab)

But this causes a visible animation when loading the page. The same happens on the linked codepen https://codepen.io/metalsadman/pen/OJyoYPB.

Is there a better way to do it? And what's the matter with the mentioned @before-transition event?

kyloe commented 4 months ago

I tried to capture the before-transition event - but it didn't appear to be firing (this is most likely my lack of familiarity with the various layers (ng/quasar/vue) and how they interact, I was probably doing something incorrectly).

My test code to see if the event was firing ...

from nicegui import Client, app, ui, events, Tailwind
import time 

columns = [
    {'headerName':"Item", 'field': 'item', 'editable': False, 'sortable': False},
    {'headerName':"Value",'field': 'value', 'editable': True,'cellDataType':'text'},
]

rows_A = [{"item":"Item A","value":"FOO"},{"item":"Item B","value":"AAA"}]
rows_B = [{"item":"Item A","value":"FOO"},{"item":"Item B","value":"BBB"}]

@ui.page('/')
def main_page() -> None:
    grid_A = None
    grid_B = None

    async def get_data():
        msg_A = await grid_A.get_client_data()
        print(msg_A)
        msg_B = await grid_B.get_client_data()
        print(msg_B)

    with ui.tabs() as tabs:
        tab_A = ui.tab("A")
        tab_B = ui.tab("B")

    def mounted():
        print("Mounted")

    with ui.tab_panels(tabs,value=tab_A).on('before-transition',mounted) as panels:
        with ui.tab_panel(tab_A) as panel_A:
            with ui.element('div').props('ng-if'):
                grid_A = ui.aggrid({
                                    'columnDefs': columns, 
                                    'rowData': rows_A,
                                    'domLayout': 'autoHeight',
                                    'stopEditingWhenCellsLoseFocus': True
                                }).style('width: 300px')  
        with ui.tab_panel(tab_B) as panel_B:
            with ui.element('div').props('ng-if'):
                grid_B = ui.aggrid({
                                    'columnDefs': columns, 
                                    'rowData': rows_B,
                                    'domLayout': 'autoHeight',  
                                    'stopEditingWhenCellsLoseFocus': True
                                }).style('width: 300px')  

    ui.button("Get data").on('click', lambda: (get_data()))

ui.run(storage_secret='THISISABIGSECRET') 
kyloe commented 4 months ago

Interesting news, @kyloe!

So as far as I understand, they suggest to traverse the tabs once they are mounted. We could wrap the Quasar tab components in order to add such code to the mounted hook. But I managed to achieve something similar by adding code to TabPanel.__init__:

class TabPanel(DisableableElement):

    def __init__(self, name: Union[Tab, str]) -> None:
        # ...

        assert isinstance(context.slot.parent, TabPanels)
        panels: TabPanels = context.slot.parent
        current_tab = panels._props['model-value']
        panels.run_method('goTo', self._props['name'])
        panels.run_method('goTo', current_tab)

But this causes a visible animation when loading the page. The same happens on the linked codepen https://codepen.io/metalsadman/pen/OJyoYPB.

Is there a better way to do it? And what's the matter with the mentioned @before-transition event?

@falkoschindler

If the property 'animated' is removed when the tabs are instantiated - then the creation process is a lot less unpleasant

with ui.tab_panels(ui_tabs, value=tabs['names'][0]).props(remove='animated') as ui_panels:

I'm still looking for a more elegant solution ...