Skip to content

Introduce API to build Single Page Applications (SPAs) #2811

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

Draft
wants to merge 153 commits into
base: main
Choose a base branch
from

Conversation

Alyxion
Copy link
Contributor

@Alyxion Alyxion commented Apr 3, 2024

NiceGUI is for us at Lechler a really awesome solution and step forward from Streamlit in regards of the visualization of live and streaming data as it puts the dev far more in control of which sub elements and page regions are updated when. This PR started with many ideas and concepts to add crucial features needed by Lechler. It now focuses on providing a first mergeable implementation to develop Single Page Applications (SPAs).

Open ToDos/Topics we need to adress

  • simplify API by allowing Outlet without yield (and then get rid of OutletView and rename Outlet to Content or similar); see Simplify API by merging Outlet and View Alyxion/nicegui#4 for WIP
  • remove code to yield parameters etc -- we can add it in a later PR if neccessary
  • Nested ui.content definitions can't be async yet.
  • put all routing into a single place: not some in Content and similar stuff in SinglePageTarget; and do we have to do separate routing in frame.js?
  • rethink architecture of "builder routing" as opposite to "real routing" via backend (ui.navigate) or frontend (ui.link)
    • maybe get rid on on_navigate in favour of a class registration in the style of Starlet middlewares?
    • There are situations when you can already see in the Outlet than building the view can't succeed, e.g. you already detect the db is down, user is not authed etc. etc. In such siuations it might definitely make sense to be able to block all views / dont try to build them. Either via an Exception or e.g. returning rather than yielding.
  • simplify or get rid of SinglePageTarget.valid member variable
  • Quite regularly we see the "wait for the client.connected" timeout. Why? And how to do better?
  • find a better name for user_data (it's more analog to parameters in function calls)
  • buildier_utils and SinglePageTarget have similar if/else instance checks; we should consolidate
  • write documentation
  • cleanup examples (remove old SPA, improve/simplify the new "complex" one)

@Alyxion Alyxion changed the title Support for per browser-tab session data and a persistent per-session between page changes Support for per browser-tab session data and a persistent per-session connection between page changes Apr 3, 2024
Michael Ikemann and others added 11 commits April 3, 2024 14:36
Integrated single page app into Client.open so navigation to SPA pages is redirected.
Fixed bug with forward and backwards navigation between SPA pages.
Collecting data from original pages to able to apply the original page title.
Integrated single page app into Client.open so navigation to SPA pages is redirected.
Fixed bug with forward and backwards navigation between SPA pages.
Collecting data from original pages to able to apply the original page title.
Fixed a bug which could occur when open was called before the UI was set up
…s registry in Client.

General clean-up
Added titles to sample app
Added docu to SPA
@Alyxion Alyxion force-pushed the feature/client_data branch from 4b89fb3 to 13f29ac Compare April 3, 2024 12:40
@falkoschindler falkoschindler requested a review from rodja April 3, 2024 14:06
…the structure of the root page, the possibility to override the class and implement custom routing and to react to the creation of sessions.

* Added samples for the single page router
* Refactored the Login sample with the new possibilities and making use of Pydantic as an example for a cleaner code base
@rodja
Copy link
Member

rodja commented Apr 4, 2024

Thank you @Alyxion. There are a lot of interesting things in here. While you are right that a certain kind of applications my want to combine SPA and "per tab storage", it would make review and discussions simpler if you could create two separate pull requests for these features.

…in the current Client instance - which in practice means "per browser tab".
@Alyxion
Copy link
Contributor Author

Alyxion commented Apr 4, 2024

Hello @rodja, sure. As this Pull Request here is anyway just a draft yet and the amount of changes of the per-tab data was manageable I extracted the lines into the separate request #2820 and added a unit test to it. As this one is dependent on the other I will keep it the way it is for now.

@Alyxion
Copy link
Contributor Author

Alyxion commented Feb 22, 2025

I fixed some formatting and made code compatible with our ruff rules. Also I looked through the tests and added comments/suggestions here and there.

Thank you for the optimizations, its taking shape.

As you saw I massively simplified Frame to move nearly all logic to a single location in the SinglePageRouter.

Other than that there were still some routing issues between different outlets, within outlets hierarchies etc. I fixed and wrote tests to ensure it stays that way.

Other than that there are still two missing feautures on my todo list:

  • Nested outlets can not use async yet. As all other combinations can this should be possible as well before merging this.
  • There are situations when you can already see in the Outlet than building the view can't succeed, e.g. you already detect the db is down, user is not authed etc. etc. In such siuations it might definitely make sense to be able to block all views / dont try to build them. Either via an Exception or e.g. returning rather than yielding.

One thing is also that I quite regularly see the wait for the client.connected to timeout. It does not have any negative impact and I also know for sure that the client connected successfully within this time as I need to receive its data etc. - this make it especially odd. If you have any idea what might lead to this this were appreciated.

I also btw once more reviewed your question above regarding user_data and yielding vs for example just storing it in the client storage. The major difference is that these variables are always bound to the context to which they are created, so the SinglePageRouter.

So e.g you have main_outlet.sub_outlet and yield something in suboutlet it can get automatically garbage collected once navigating to main_outlet.other_sub_outlet as the SinglePageRouter of the sub outlet is released and so all content of the user_data. So basically acting like function variables passed to nested functions.... w/o the need to do so and write cleaner, oop versions.

Some intense weeks ahead, so beyond major bugfixes I will likely continue on this end of March, doing the last refinemenets and a summary of the classes and changes involved as discussed above.

@rodja
Copy link
Member

rodja commented Feb 22, 2025

I started a checklist of open topics and todos in the description of this PR. That way we can better track what we still need to do.

@rodja
Copy link
Member

rodja commented Feb 23, 2025

I created Alyxion#4 which merges Outlet and View in favour of a unified Content API.

@Alyxion
Copy link
Contributor Author

Alyxion commented Feb 23, 2025

I created Alyxion#4 which merges Outlet and View in favour of a unified Content API.

Thanks for the changes. As written above already I'm quite skeptical about unifying the name completely - though I like the possibility allowing not to yield, e.g. in the just mentioned example that the outlet already decides / detects, that further nesting shall anyway be blocked b/c of a not available database or similar situations.

We have a quite large app meanwhile, 3 outlets, dozens of views where the outlet always also defines some kind of "gate" - ensuring oauth, ensuring DBs, initiating per-user instance variables etc, preparing shared ui elements and at least for myself I can say that having here... also in the source code... a clear visual differentiation, being able to search for all outlets in the code etc. feels cleaner than just naming them all the same - even though they effectively very different roles in practice.

@rodja
Copy link
Member

rodja commented Feb 24, 2025

As written above already I'm quite skeptical about unifying the name completely [...]: a clear visual differentiation, being able to search for all outlets in the code etc. feels cleaner than just naming them all the same - even though they effectively very different roles in practice.

The roles are not so different. Both hold UI elements, both contain "layout". The only difference is that the leafs do not contain sub-content which can be changed via routes. If your app is big and you need that differentiation, you can always introduce your own function naming scheme -- or structure with submodules. I think the API should be as simple as possible and not enforce to many concepts. Especially for new users.

[...] where the outlet always also defines some kind of "gate" - ensuring oauth, ensuring DBs [...]

In my understanding, this is a different topic and has nothing to do with Outlet/View vs Content. In the checklist I named it "rethink architecture of 'builder routing' as opposite to 'real routing' via backend". I would like to try using an approach like FastAPI/Starlette Middlewares instead of on_navigate parameter -- but that is still a few weeks in the future I guess.

@Alyxion
Copy link
Contributor Author

Alyxion commented May 11, 2025

One major remark here which came up in our projects:

For most web pages its is common to have the mai landin page at the domains or subdomain's root, so as a classic nicegui page via ui.page('/').

When "reserving" the root via ui.outlet('/') this currently has the effect - due to the way that currently basically /* is reserved - no other endpoint or router can be registered afterwards anymore at all, also including for example the ui.upload component.

One way to solve it were not using placeholders at all when registering the main outlet but adding one router per Outlet.view aka nested sublets. Ideas?

@rodja
Copy link
Member

rodja commented May 25, 2025

We at Zaubereug are still totally committed to get SPAs into NiceGUI. As discussed a few weeks ago in a video call with @Alyxion we are therefore taking lead in getting this huge PR into a mergable state. First order of business is the integration of Alyxion#4 which combines ui.outlet and ui.view into ui.content. I find it to be much simpler than the outlet/view dualism. We will also update the description and todos at the top to provide a up-to-date summary of the progress.

@rodja
Copy link
Member

rodja commented May 25, 2025

I removed the following part from the description because it does not describe the goal of this PR:


NiceGUI is still lacking three for us very crucial features Streamlit offers:

  • A persistent WebSocket connection for the whole user session (per tab)
  • A data storage possibility per tab to enable the user to create multiple app instances.
  • In-memory storage of complex objects (e.g. pandas data tables)

image

This (still work in progress) pull request tries to resolve at least most of the points above. It shall not yet resolve the situation that a user has an unstable internet connection and thus looses the connection to a server completely and needs to reconnect.

Persistent connection

In a scenario where you want to serve your NiceGUI solution not to hundreds of users there is after a certain point no way around scaling the solution over multiple processes, CPUs or over multiple servers.

If you need to load/keep alive large amounts of data per user w/o involving an external database this requires that the whole user session, even between page changes, is bound to one single process on one specific server. Streamlits uses a SPA approach here thus it creates a WebSockets connection once and all follow-up page and tab changes are just virtual thus changing the URL and browser history in the browser using pushstate but never really loading a new page using GET.

As discussed in the Add app.storage.tab or similar (1308) and in Discord there are several use cases where this is crucial to retain in-memory data on a "per session" basis, see below, which consequently requires that there is such a session in the first place.

Per tab storage

A data storage possibility per tab is cruicial to enable the user to create multiple app instances with different login credentials, configurations and views on a per tab basis. This is on purpose volatile so that user-credentials, critical business data etc. are gone once the browser tab was closed and the connection timed out. This shall match the current behavior of st.session_state.

In-memory storage of complex objects

The possibility to store living, non-JSON compatible objects such as Pandas tables, ML model weights etc. on a "per tab" basis and make them as easy accessible among different pages, global helper classes etc. as currently app.storage.user.

Update: Extracted the app.storage.session feature into a separate pull request 2820

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

Successfully merging this pull request may close these issues.

5 participants