Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blank page #72

Closed
4l1fe opened this issue Aug 28, 2022 · 4 comments
Closed

Blank page #72

4l1fe opened this issue Aug 28, 2022 · 4 comments
Assignees

Comments

@4l1fe
Copy link

4l1fe commented Aug 28, 2022

Problem

Well, i read the source code and have many words to say. In short, i don't understand the concepts you implemented in terms of OOP.

What i'd like to have is to organize my project with importing ui object from a module of initializing all the elements and some application logic which adds a few new elements after a request to a DB. Then call method ui.run() in a top level module. This approach shows a blank page.

If i keep ui object in one module everything is working, but it's a bad practice and in my case i took nicegui as a fast WUI builder for a representation layer over some logic in application layer.

Pseudo code to explain

repr_layer.py

from app_layer import logic
from nicegui import ui

def add_elements(container):
   with container:
       result = logic()
       container.label(result)
   container.update()


def initialize_ui():
    ui.label('Any label')
    row = ui.row()
    ui.button(on_click=lambda: add_elements(row))

    return ui

run.py

from repr import initialize


if __name__ == '__main__':
    ui = initialize()
    ui.run()

And nothing is displayed here, but a server is running normally.

Expectations

Hope nicegui can be used along with some custom logic beside WUI implementation? Or it's not intended for such cases by design and better to take justpy or streamlit, for exmaple?

@falkoschindler falkoschindler self-assigned this Aug 29, 2022
@falkoschindler
Copy link
Contributor

Hi, thanks for reporting this issue! I think I can explain what is going on an how to fix your example.

By default NiceGUI starts a uvicorn server with auto-reload activated. This already happens during import and that's why the main script (run.py in your case) blocks within from nicegui import ui and never reaches the main guard. The uvicorn reloader takes over and runs your script, but the reloader is not __main__. We chose this approach because we like working with auto-reload very much and wanted to avoid evaluating the main script twice, once with the server and once with the auto-reload mechanism.

You can either remove the main guard or at least move ui = initialize() one line up. Or you disable auto-reload with ui.run(reload=False). Then the server is only started during ui.run and the control flow is as expected.

With some clean up here and there, this is your resulting code:

repr_layer.py

from nicegui import ui

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

def initialize_ui():
    ui.label('Any label')
    row = ui.row()
    ui.button('+', on_click=lambda: add_elements(row))
    return ui

run.py

from repr_layer import initialize_ui

if __name__ == '__main__':
    ui = initialize_ui()
    ui.run(reload=False)

Notes:

  • no need to import ui in run.py
  • no need for container.update() (happens automatically when leaving the container scope)
  • ui.label instead of container.label
  • added '+' (just because why not)

Let me drop a few thoughts about the OOP design decisions you mentioned.

Implicit nesting via scopes

We wanted NiceGUI to be an intuitive API for creating user interfaces. UIs are typically hierarchically organized and markup languages like HTML reflect that visually:

<div id="container">
    <div id="box">
        <p>Hello world!</p>
    </div>
</div>

That's why we wanted a UI description with NiceGUI to have a similar form:

with ui.card():
    with ui.row():
        ui.label('Hello world!')

JustPy, on the other hand, becomes tricky to read when nesting UI elements:

container = Div()
box = Div()
label = Div(text='Hello world!')
box.add(label)
container.add(box)

(Sure, you could create some objects inline. But if you need their references, it clutters quickly.)

Thus, with NiceGUI you don't create hierarchy via parent/children arguments or add/insert methods, but via indentation and scoping. I.e. using a with statement opens a scope and new elements are automatically nested there. This is unusual, but turns out very handy.

Constructors as functions with side-effects

A function like ui.label is actually a constructor of a Label object. But we chose to use lowercase names in order to indicate the function-like behavior. Besides creating an object ui.label, modifies the UI. In many cases, you don't event need the object reference.

Again, compared to an approach like JustPy, we think NiceGUI is more convenient.

Comparison to Streamlit

Streamlit and its weird control flow was actually the motivation for us to write NiceGUI. See #21 for a more detailed discussion.

@4l1fe
Copy link
Author

4l1fe commented Aug 31, 2022

Thank you for the details. I tested both ways out.

Not working

With the reloading disabled a web server isn't getting started.

from repr_layer import initialize_ui

if __name__ == '__main__':
    ui = initialize_ui()
    ui.run(reload=False)

Working

With the main guard removed it works. That's my choice.

Comment on the concept

That's why we wanted a UI description with NiceGUI to have a similar form:

with ui.card():
    with ui.row():
        ui.label('Hello world!')

You know, it's quite intuitive and i like this approach, but let me ask you why do we use ui object in nesting? In my opinion, subelements should be explicitly added to their parent one, label to row in the example above, but not to the root ui element:

with ui.card() as card:
    with card.row() as row:
        row.label('Hello world!')

Now, from an html perspective, i see adding subelements as follow:

<body>  <!-- let's suppose that's `ui` element   -->
  <subelement>  <!-- code line such as `with ui.row(): ui.subelement()` in my mind is placed here and it is NOT correct-->
  <div id="container">
      <div id="box">
          <p>Hello world!</p>
          <subelement>  <!-- would code line `with ui.row() as row: row.subelement()` in your mind places an element here? --> 
      </div>
  </div>
</body>

Anyway, i enjoy Nicegui. Get my comment as a review only 🙂

@falkoschindler
Copy link
Contributor

The "not working" example using reload=False works for me. You might just need to restart the script, since changes to ui.run arguments are ignored during auto-reload. Or maybe there's something else I'm missing. Glad to here that it works without the main guard.


Regarding your comments on our concept: Thanks for the interesting thoughts! It's quiet fun to juggle with the different possibilities a languange like Python gives you to create an API.

Let me elaborate a bit on your example:

with ui.card() as card:
    with card.row() as row:
        row.label('Hello world!')

I see your point. The ui.scene container is actually an example where we followed this approach. In order to separate the list of 3D objects from the "regular" UI elements, they are created like

with ui.scene() as scene:
    scene.sphere()

So introducing a new 3D object label would not conflict with ui.label. But for UI elements we chose to use the short namespace ui. Consider the following example with some more containers:

with ui.card() as main_card:
    with card.row() as info_row:
        info_row.label('Time:')
        info_row.label('CPU%:')
        info_row.label('RAM%:')
    with card.row() as button_row:
        button_row.button('Start')
        button_row.button('Pause')
        button_row.button('Stop')

Repeating the container names is pretty verbose, given the indentation already defines the hierarchy. Sure, you could use names like row1 and row2 or r1 and r2, but this isn't very readable and your namespace get's spoiled quickly. In large UIs you might have to search for free variable names. Or you use the same row for each row, but I'm not sure if this is good practice. And changing the layout (e.g. from row to column) requires you to change row into column many times.

Besides that, your approach would clutter the namespace of, e.g., a card: Is card.color a property or does it create a new UI element of type color? Your IDEs auto-suggestions will become less helpful.

@4l1fe
Copy link
Author

4l1fe commented Sep 2, 2022

Well, i pretty understand avoidance of verbosity, var names cluttering and building UIs through ui object only. My point was actually more about pythonic explicit way of coding PEP20

>>> import this
The Zen of Python, by Tim Peters

...
Explicit is better than implicit.
...

I think if you mention the concept with a few details in the documentation(your explanation above, for example) it would keep people away of being a bit confused on it as they can follow the idea. I mostly mean a python developers category.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants