Closed me21 closed 2 years ago
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.
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?
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:
on_connect
is fired.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 await
s for 1 second. NiceGUI, in turn, await
s 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.
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()
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
...
Ok, in commit https://github.com/zauberzeug/nicegui/commit/ea05fc6368e56c69ff31df73f38aec1ab160244b 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()
@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!?
@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.
@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
.
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'
#!/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?
@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...
@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.
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:
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'
#!/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
)
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
...
I have a page with
on_connect
handler. In this handler, I start some tasks withasyncio.create_task
:Several tasks are ongoing and the last of them is single-shot. They all are async as they request and receive values from MQTT broker. What I experience is that I receive the message from MQTT broker in the last task, but the corresponding UI elements are sometimes not updated.
I suspect this is due to the asynchronous nature of the task:
Please share your thoughts on the matter - am I right? Is it possible to check it somehow?
I currently fixed it by
await
ing the single-shot task instead of putting it to the common list of running tasks. Is this fix good or it's a hackish way to work around the problem?