Advanced operations and techniques - zauberzeug/nicegui GitHub Wiki
For advanced users, the following includes some advanced operations and techniques.
- Some may be left out of the NiceGUI library for a vatiety of reasons, but that's not to say they don't work. Just that, we know so far it can be done with a singular version of NiceGUI.
- Some may be clever uses of lesser-known library functions, which enables something special to be done.
All entries should have:
- Link to original discussion
- Code snippet (optionally live demo)
- Simple explanation
- Tested versions (or, anecdotally working)
- Discussion on integration into NiceGUI, if any
https://github.com/zauberzeug/nicegui/issues/4403
from nicegui import ui
ui.add_css('''
.nicegui-markdown a {
color: green;
text-decoration: none;
}
.nicegui-markdown a:hover {
text-decoration: underline;
}
''')
ui.markdown('''
This is a [link](https://example.com).
''')
ui.run(port=8040)
Use
.nicegui-markdown a
to target links inside the NiceGUI markdown element.
Reported to be working.
Integration into the library was not discussed, but generally CSS hacks are not integrated.
https://github.com/zauberzeug/nicegui/discussions/4519
from nicegui import ui, app
def unified_header(header_str, back_location=""):
username = app.storage.user.get('username', '')
is_admin = app.storage.general.get("user_pw", {}).get(username, {}).get('admin', False)
# lock emoji prefix if is admin
prefix = "🔒" if is_admin else ""
with ui.header().classes("w-full bg-gray-100 p-4"):
with ui.row().classes("w-full"):
if back_location:
ui.button("Back", on_click=lambda: ui.navigate.to(back_location)).classes("shrink-0")
ui.label(prefix+header_str).classes('text-2xl flex-grow font-bold text-black')
with ui.row().classes("shrink-0") as injectable:
pass
return injectable
@ui.page('/')
def main_page():
def logout() -> None:
app.storage.user.update({'authenticated': False})
ui.navigate.to("/login")
with unified_header("Main Page"):
ui.button("Logout", on_click=logout).classes("shrink-0")
with unified_header("Main Page"):
is no different towith injectable:
Tested on: NiceGUI 2.9.0 and 2.12.1
@contextmanager
def my_item(text: str, icon: str):
with ui.row(align_items='center').classes('border p-4 w-64'):
ui.icon(icon)
ui.label(text)
yield
with my_item('Jane', 'face'):
ui.button('Call').props('flat')
Only works when you use the context, i.e.
with
into it. Simply callingmy_item
without thewith
keyword would not work.
Tested on: N/A. Reported to be working.
class MyItem(ui.row):
def __init__(self, text: str, icon: str) -> None:
super().__init__(align_items='center')
self.more = ui.element()
with self.classes('border p-4 w-64'):
ui.icon(icon)
ui.label(text)
self.more.move(self)
def __enter__(self):
super().__enter__()
return self.more
def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
return False
MyItem('Paul', 'face')
MyItem('John', 'face')
with MyItem('Jane', 'face'):
ui.button('Call').props('flat')
The class itself is a valid element, while the
__enter__
and__exit__
methods simply makeswith MyItem('Jane', 'face'):
equivalent towith {the_inner_element_of_MyItem}:
by returningself.more
in the__enter__
Tested on: N/A. Reported to be working.
https://github.com/zauberzeug/nicegui/discussions/4590#discussioncomment-12756679
class Slot(ui.element):
...
class MyItem(ui.row):
def __init__(self, text: str, icon: str) -> None:
super().__init__(align_items='center')
with self.classes('border p-4 w-80'):
ui.icon(icon)
self.slot_a = Slot()
self.slot_b = Slot()
ui.label(text)
MyItem('Paul', 'face')
MyItem('John', 'face')
item = MyItem('Jane', 'face')
with item.slot_a:
ui.button('Call').props('flat')
with item.slot_b:
ui.button('Text').props('flat')
Similar to method 1, but enables essentially returning more than 1 element for which you can
with
into.
Tested on: N/A. Reported to be working.
Inclusion into NiceGUI was not discussed for neither of these 4 methods.
https://github.com/zauberzeug/nicegui/discussions/4393#discussioncomment-12509427
ui.add_head_html('''
<style>
.extra-compact, .extra-compact .q-field__control, .extra-compact .q-field__append, .extra-compact .q-field__control--addon {
height: 30px !important;
max-height: 30px !important;
min-height: 30px !important;
align-items: center;
}
.q-field__control-container {
display: flex;
align-items: center;
}
.extra-compact .q-field__label {
top: 6px !important;
}
</style>
''', shared=True)
Styling is applied via abusive use of DevTools and targeting element by class until the height is alright.
Tested on: NiceGUI 2.12.1, 2.13.0. Hypothetically should work across NiceGUI 2.x release (same version of Quasar)
Left out of the library for to code simplicity and avoid unforseen side-effects.
https://github.com/zauberzeug/nicegui/pull/4480
@app.exception_handler(Exception)
async def _exception_handler_500(request: Request, exception: Exception) -> Response:
with Client(ui.page(''), request=request) as client:
ui.label('This is my custom error page')
ui.log().push(traceback.format_exc(chain=False).strip())
return client.build_response(request, 500)
Leverages the
@app.exception_handler(Exception)
decorator, which can return a NiceGUI page response if you dowith Client(ui.page(''), request=request) as client:
andreturn client.build_response(request, 500)
. Currently works for sync page functions, albeit a long callstack.
Tested on: NiceGUI 2.12.1, 2.13.0.
Native functionality left out of NiceGUI because it's not a good idea to show backend stacktraces on the web site error page (potential security / privacy issues). However,
https://github.com/zauberzeug/nicegui/discussions/4514#discussioncomment-12579619
def notify() -> None:
action = {
"label": "View",
"color": "white",
":handler": "() => emitEvent('view-clicked')",
}
ui.notification("Hello!", actions=[action])
ui.button('Notify', on_click=notify)
ui.on("view-clicked", lambda: print("This worked!"))
This leverages the Quasar additional keyword arguments of
ui.notification
to directly interface with the Quasar Notify API, while events are used to trigger Python callback from the browser.
Tested on: N/A. Reported to be working.
Native support is on discussion, but the issue was "not knowing where to place it", with ui.notification_action
being a candidate, only that it was misleading as it was not a UI element.
https://github.com/zauberzeug/nicegui/discussions/4516
from nicegui import app, ui, context
@app.get('/download')
async def download(client_id: str):
def generate_dummy_data():
for _ in range(384): # Will generate ~3MB (384 * 8KB)
chunk = ''.join(chr(randint(65, 90)) for _ in range(8192))
yield chunk
with Client.instances[client_id]:
ui.notify('Download complete')
return StreamingResponse(
generate_dummy_data(), media_type='text/plain',
headers={'Content-Disposition': 'attachment; filename=dummy_data.txt'}
)
@ui.page('/')
def main():
ui.button('download', on_click=lambda: ui.download(f'/download?client_id={context.client.id}'))
ui.run()
This cleverly leverages the StreamingResponse of FastAPI such that actions can be taken on the server side when the download begins, ends, or at any intermediate progress.
Tested on: N/A. Reported to be working.
Integration into NiceGUI was not discussed explicitly, but one of the issue was that browsers don't provide a reliable native event that notifies when a user-initiated download has completed to disk, owing to security and privacy considerations.
Not exactly rocket science. Check out how it is done by broh5, which is a "(Bro)wser-based GUI (H)DF(5) Viewer in Python". https://github.com/algotom/broh5/blob/f7dfe6e72859e40c2ec529eb1dd5ab0d62641c12/broh5/lib/interactions.py#L114-L128
(Code will need some rework in order to be used outside of a class)
def __get_xy(self, x, y, ax):
try:
xn, yn = ax.transData.inverted().transform((x, y))
return xn, yn
except Exception as e:
return None, None
def mouse_handler(self, e: events.MouseEventArguments):
"""
Show the zoomed area around the mouse-clicked location or the
intensity profile across the clicked location.
"""
if self.image is not None and (
self.enable_profile.value or self.enable_zoom.value):
x, y = self.__get_xy(e.args['offsetX'], e.args['offsetY'], self.ax)
The magical line:
xn, yn = ax.transData.inverted().transform((x, y))
Tested on: Apparently works since it is used by broh5
Native support is not planned due to trivial nature of the operation, as well as unfairness towards matplotlib amongst the many chart types.
https://github.com/zauberzeug/nicegui/discussions/4501
Code is at repo: https://github.com/depley/nicegui-custom-echarts (link to external content)
You will be trading off proper animation support, to avoid rendering the chart twice: https://depley.github.io/nicegui-custom-echarts/ (link to external content)
Tested on: N/A. Reported to be working.
Native support is on discussion, with the goalpost for inclusion being a solution which is best-of-both-worlds. Switching between the two modes based on whether animationDuration was set in config was the best solution so far, with no need to add an extra parameter. However, everything is still in consideration and planning phase.
https://github.com/zauberzeug/nicegui/discussions/4505
from nicegui import ui
ui.add_head_html("""
<style>
body { background-color: #121212; color: white; }
</style>
""", shared=True)
@ui.page("/dark_page")
def dark_page():
ui.input("Welcome to the dark side")
ui.link("Visit dark page", dark_page)
ui.run(dark=True)
By force-set the page color using
ui.add_head_html
, or better,ui.add_css
, the page color is well-defined at thehead
stage of the page, avoiding the flash of default white page color despite however long the page load may take.
Tested on: 2.12.1
Integration into NiceGUI was not discussed.