Advanced operations and techniques - zauberzeug/nicegui GitHub Wiki

Introduction

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

Styling and layout

Styling elements inside markdown

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.

Custom component supporting with to put elements inside

https://github.com/zauberzeug/nicegui/discussions/4519

Method 1: returning an element which supports with

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 to with injectable:

Tested on: NiceGUI 2.9.0 and 2.12.1

Method 2: @contextmanagaer with yield for additional content

@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 calling my_item without the with keyword would not work.

Tested on: N/A. Reported to be working.

Method 3: use a class which supports both with and no with

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 makes with MyItem('Jane', 'face'): equivalent to with {the_inner_element_of_MyItem}: by returning self.more in the __enter__

Tested on: N/A. Reported to be working.

Method 4: Return object with several opportunities to with into

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.

Force a smaller-than-dense height for Quasar components

image

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.

Verbose logging

Custom exception handler with traceback (replace the Sad Guy page)

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 do with Client(ui.page(''), request=request) as client: and return 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,

Interactivity

ui.notification with actions

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.

Trigger an event when ui.download completed

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.

Get back data coordinate from clicking ui.matplotlib

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.

Pursuit of performance

Apache EChart without render-twice issue

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.

Combat page color flash when using non-white page color

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 the head 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.

⚠️ **GitHub.com Fallback** ⚠️