Code Monkey home page Code Monkey logo

Comments (12)

me21 avatar me21 commented on May 16, 2024 1

Or maybe just call on_connect only after the websocket is connected and thus the page is fully functional. Or leave on_connect as is and provide another handler on_websocket_connect...

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024 1

page_stack and view_stack

They are used internally to keep track of which context is currently open and, thus, where to add an element. If you write

with ui.card():
    ui.label('A')
    with ui.row():
        ui.label('B')
        ui.label('C')
    ui.label('D')

first a ui.card is added to the view_stack. Therefore, the label "A" is added to this card. After entering ui.row, the next two labels are added to the new top element, i.e. the row. After leaving and popping the row again, label "D" is added to the card.
The page_stack behaves similarly.

Changing the main page

NiceGUI is intended to be easy to start with. So a three line hello world

from nicegui import ui
ui.label('Hello world!')
ui.run()

should simply work. This implies quite a lot (yes, we know, explicit is better than implicit...), e.g. how the server is started and that there is a main page at route "/". This page is automatically pushed to the page_stack and remains there. If an element is not explicitly added to a different page, it is added to "/".

Changing the main page is currently a bit tricky. You can get a reference from the first item on the page_stack:

from nicegui.globals import page_stack

main_page = page_stack[0]
main_page.dark = True
with main_page:
    ui.label('Hi!')

On the other hand it should also be ok to create a new page with route "/". Yes, then there are two routes with path "/", but since new pages insert their routes before others, it should work.

with ui.page('/', dark=True):
    ui.label('Hi!')

It also might be useful to define a MainPage class:

class MainPage(ui.page):
    def __init__(self):
        super().__init__('/', dark=True)
        with self:
            ui.label('Hi!')

MainPage()

I'll think about whether NiceGUI should automatically remove routes that are overwritten by new pages. The implementation, however, could be challenging, since we would also need to replace the page in the page_stack...

from nicegui.

me21 avatar me21 commented on May 16, 2024

It appears this problem affects continuously run tasks too. If they obtain a value after the page is loaded, but before the websocket is connected, then this value is not displayed in the UI too. And it won't be shown until it changes again - NiceGUI is smart enough to skip unnecessary websocket messages when the value is unchanged.

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024

Your question might be related to the breaking change in version 0.8. Before, we made use of the underlying JustPy framework to automatically update the UI after event handlers (if not actively prevented with a return False at the end). Now NiceGUI still updates most elements automatically (e.g. a label is updated after setting its text property), but some updates you might have to trigger explicitly yourself. Can you give more details about what should happen in the UI? Or maybe you can boil it down to a minimum example code?

The following example shows the current date and time after a new client has connected. Although there's a delay of 1 second (and the page is updated and rendered), setting label.text triggers an update for this UI element. Its state is sent to the client which updates the UI in the browser.

import asyncio
from datetime import datetime

from nicegui import ui

async def connect():
    await asyncio.sleep(1.0)
    label.text = datetime.now().isoformat()

with ui.page('/page', on_connect=connect):
    label = ui.label()

ui.run()

But probably you're aware of all that and the question is deeper: Is it possible that an update does not reach the client because the connection is not yet established? As far as I understand, the complete state of a page is held on the server (in Python world) for new clients. When they connect, they get the complete state of that page. I don't see how they could miss an update.

In your example the tasks are even only started after a new connection is created. So how should this connection not be ready when the MTTQ response updates the UI?

You write "but if the response comes between the page has been loaded, but before the websocket connection is up, then the component update is lost." - What do you mean with "loaded"? Doesn't the client "load" the page after establishing a websocket connection?

from nicegui.

me21 avatar me21 commented on May 16, 2024

In your example the tasks are even only started after a new connection is created. So how should this connection not be ready when the MTTQ response updates the UI?

There are actually two connections: HTML connection and websocket connection. The on_connect handler is fired when the HTML connection is established, but obviously before the websocket connection, because the websocket is created by the page's javascript.

You write "but if the response comes between the page has been loaded, but before the websocket connection is up, then the component update is lost." - What do you mean with "loaded"? Doesn't the client "load" the page after establishing a websocket connection?

In my understanding, on_connect handler is called when the browser requests the page. This means that the websocket connection is not yet established, because it needs the page to be fully loaded in the browser, including javascript, because websocket connection is started by javascript.

So, as I see it, the process is as follows:

  1. We open new tab in the browser and open the NiceGUI webpage in it.
  2. The browser requests the page from NiceGUI server.
  3. on_connect is fired.
  4. In my example, async tasks are started.
  5. NiceGUI prepares HTML file with the initial components state and sends it to the browser.
  6. Browser starts executing the javascript on the webpage and starts websocket connection.

What would happen if the MQTT response that updates the UI arrives after 5., but before 6. is completed?

Your example with current date and time is slightly different - your on_connect handler awaits for 1 second. NiceGUI, in turn, awaits for the on_connect to finish before returning from _route_function, thus sending HTML code.
In my example, the new tasks are started, so that on_connect returns immediately. These tasks will kick in when there's another await down the road, probably when the HTML response is streamed to the browser.

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024

Oh, I see! on_connect is called as part of the routing generating the HTML page, "way" before the socket connection is created. You're absolutely right: Changes to the UI that happen after on_connect returns and before the socket is connected do not reach the client.

Here is a reproduction:

import asyncio
from nicegui import ui

async def on_connect():
    label.text = 'loading...'
    asyncio.create_task(takes_a_while())

async def takes_a_while():
    await asyncio.sleep(0.1)
    label.text = 'done'

with ui.page('/page', on_connect=on_connect):
    label = ui.label()

ui.run()

With a delay of 0.1s the text "done" never appears. It's the same when removing the delay completely. But a delay of 1.0s works on my machine.

I have to think about if and how we could fix this problem. Maybe we can somehow keep track of UI updates on pages that are still connecting. These updates should be delivered once the socket is open.

As a workaround I would add a delayed UI update to make sure there is an update after the socket connected. This might be a component update (e.g. label.update()) or even a whole page update (await label.page.update()).

async def takes_a_while():
    await asyncio.sleep(0.1)
    label.text = 'done'
    await asyncio.sleep(1.0)
    label.update()

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024

Ok, in commit ea05fc6 I introduced an on_page_ready argument for ui.page that is called when the websocket is connected. The name is borrowed from the corresponding JustPy event.

Using on_page_ready the reproduction from above is working as expected. The task is only started after the websocket is fully connected and the changing label text (from "loading..." to "done") is correctly synchronized with the client.

import asyncio
from nicegui import ui

async def on_page_ready():
    label.text = 'loading...'
    asyncio.create_task(takes_a_while())

async def takes_a_while():
    await asyncio.sleep(0.1)
    label.text = 'done'

with ui.page('/page', on_page_ready=on_page_ready):
    label = ui.label()

ui.run()

from nicegui.

hroemer avatar hroemer commented on May 16, 2024

@falkoschindler I am trying to wrap my head around the design of nicegui but don't quite get it (like the questions in #72). Maybe you could point to some documentation of how things work out here?

The example above assumes a global variable label, otherwise the update methods wouldn't work.

How are you supposed to change state of a component such as the label in your example above for any given larger UI?
Where are the references/names actually "stored"?

I tried to grab them from page_stack[0].instances[1].view.components but then updates to a component need to be triggered by an await component.update() where update() is actually a synchronous method, but still needs to be awaited!?

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024

@hroemer

Maybe you could point to some documentation of how things work out here?

You've probably seen nicegui.io. That's the only documentation at the moment. More detailed explanations can be found in GitHub issues and discussions. I probably should add some links in a Q&A section.

How are you supposed to change state of a component such as the label in your example above for any given larger UI?

A function like ui.label() is actually a constructor of a NiceGUI element, which registers itself with the UI. You can store the resulting object wherever you want for later reference. Here is, for example, a "WifiButton" from the library RoSys: https://github.com/zauberzeug/rosys/blob/79c2c1dd8fabd0086fa4d4231f5bd6127291006a/rosys/system/wifi_button_.py#L38. It derives from ui.button, sets the on_click event handler and some props, defines a ui.dialog and creates a ui.timer to regularly update itself and its child elements. An explicit UI update is usually not needed. This way you can encapsulate groups of UI elements into classes and use them in higher-level code.

Where are the references/names actually "stored"?

NiceGUI itself doesn't explicitly store UI elements, but only creates JustPy views. If you need the elements, you should hold them yourself as mentioned above. Usually you shouldn't need to tap into nicegui.globals like page_stack. These are primarily for internal use.

from nicegui.

hroemer avatar hroemer commented on May 16, 2024

@hroemer thanks for your reply, the WifiButton demo really is great.

Though I am still not getting a combined sample working, including on_page_ready.

sample_page.py:

import asyncio

from nicegui import ui


class SamplePage(ui.page):
    def __init__(self) -> None:
        self.label = ui.label('Any label')
        self.label.update()
        row = ui.row()
        ui.button('+', on_click=lambda: self.add_elements(row))
        with ui.card():
            with ui.row():
                ui.label('Hello world!')
        super().__init__(route="/page", title="repr page", on_page_ready=self.on_page_ready)

    def add_elements(self, container):
        with container:
            result = 'something'
            ui.label(result)

    async def on_page_ready(self):
        self.label.text = 'loading...'
        asyncio.create_task(self.takes_a_while())

    async def takes_a_while(self):
        await asyncio.sleep(1.1)
        self.label.text = 'done'

main.py

#!/usr/bin/env python3
from jpy.ng.sample_page import SamplePage

ui = SamplePage()
ui.run(reload=True,
       host='127.0.0.1',
       port=8000,
       dark=True
       )

Accessing /page opens a blank page (#72), even though the on_page_ready event properly fires. Accessing / elements defined in __init__ are rendered. If I change the route to / I also get a blank page.
If I change the run-method to reload=False I get an AttributeError: 'SamplePage' object has no attribute 'run' error!?

I would really like to hook up to the on_page_ready in a composed way like the WifiButton example. Is this feasible in any way?

from nicegui.

falkoschindler avatar falkoschindler commented on May 16, 2024

@hroemer Let me see...

  • The issue with setting reload to True or False is because your ui is a Page object, not a Ui. Your main.py should look more like this:

    from nicegui import ui
    from sample_page import SamplePage
    
    SamplePage()
    
    ui.run(reload=True, host='127.0.0.1', port=8000, dark=True)

    In the original code NiceGUI's pre-evaluation of ui.run (in order to know how to start the uvicorn server while importing ui) is working, although actually calling ui.run does not work. Long story short: ui should be the Ui object imported from nicegui.

    The instantiated object of type SamplePage isn't needed anymore, since it already registered itself with the UI as a side-effect. You can follow the style of NiceGUI and write it like a function and like the other UI elements:

    ...
    from sample_page import SamplePage as sample_page
    
    sample_page()
    ...
  • The initializer of SamplePage also needs some corrections:

        def __init__(self) -> None:
            # call base initializer first:
            super().__init__(route='/page', title='repr page', on_page_ready=self.on_page_ready)
            # now work within the `self` context:
            with self:
                self.label = ui.label('Any label')
                # calling `update()` is not necessary
                row = ui.row()
                ui.button('+', on_click=lambda: self.add_elements(row))
                with ui.card():
                    with ui.row():
                        ui.label('Hello world!')

    The rest should be working as expected.

Well well, NiceGUI definitely needs more documentation about building such custom elements or pages...

from nicegui.

hroemer avatar hroemer commented on May 16, 2024

@falkoschindler Thank you for the clarification, using the context managers is obviously the key here. The example is now working as expected.

I still don't understand the (internal) use of the page and view stacks, though. The page is added to the stacks on
enter and removed on exit, thus when __init__() is done in the sample page example:

def __enter__(self):
page_stack.append(self)
view_stack.append(self.view)
return self

def __exit__(self, *_):
page_stack.pop()
view_stack.pop()

If I change the page route to root path / two routes actually get registered within the Starlette route instances.
The code still runs as expected, but I fear this might have side effects.

routes

Could you please explain how "pages" are intended to be used here or specifically how to "attach" or use the default
route?

Here's the current code for reference:

sample_page.py:

import asyncio

from nicegui import ui


class SamplePage(ui.page):
    def __init__(self) -> None:
        # call base initializer first:
        super().__init__(route='/', title='sample page', on_page_ready=self.on_page_ready)
        # now work within the `self` context:
        with self:
            self.label = ui.label('Any label')
            # calling `update()` is not necessary
            row = ui.row()
            ui.button('+', on_click=lambda: self.add_elements(row))
            with ui.card():
                with ui.row():
                    ui.label('Hello world!')

    def add_elements(self, container):
        with container:
            result = 'something'
            ui.label(result)

    async def on_page_ready(self):
        self.label.text = 'loading...'
        asyncio.create_task(self.takes_a_while())

    async def takes_a_while(self):
        await asyncio.sleep(1.1)
        self.label.text = 'done'

main.py:

#!/usr/bin/env python3
from nicegui import ui
from sample_page import SamplePage as sample_page

sample_page()

ui.run(reload=True,
       host='127.0.0.1',
       port=8000,
       dark=True
       )

from nicegui.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.