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

Is it possible to not share the state between multiple browser windows? #6

Closed
qiuwei opened this issue Jan 7, 2022 · 9 comments
Closed
Labels
enhancement New feature or request

Comments

@qiuwei
Copy link

qiuwei commented Jan 7, 2022

In the button example,

from nicegui import ui

def button_increment():
    global button_count
    button_count += 1
    button_result.set_text(f'pressed: {button_count}')

button_count = 0
ui.button('Button', on_click=button_increment)
button_result = ui.label('pressed: 0')

ui.run()

How can I have separate button counts for different multiple browser windows?

@rodja
Copy link
Member

rodja commented Jan 10, 2022

This is an interesting requirement. The short answer would be no. NiceGUI is build on JustPy, a framework which uses the python backend as frontend. So there is technically no state per browser tab. All state is kept synchronised between all clients.

But we have an idea on how to use the interaction event from one of the browser tabs to trigger an action which is only executed on the certain tab. It's quite tricky but the work had become more intense thanks to your request.

@rodja rodja added the enhancement New feature or request label Jan 10, 2022
@qiuwei
Copy link
Author

qiuwei commented Jan 11, 2022

Thanks for the information about Justpy.
I have played a bit with it. It seems that not sharing the states between tabs is the default for Justpy.

import justpy as jp

def my_click(self, msg):
    self.count += 1
    self.text = f'{self.count}'

def event_demo():
    wp = jp.WebPage()
    d = jp.Div(text='Not clicked yet', a=wp, classes='w-48 text-xl m-2 p-1 bg-blue-500 text-white')
    d.count = 0
    d.on('click', my_click)
    return wp

jp.justpy(event_demo)

@rodja
Copy link
Member

rodja commented Jan 12, 2022

Thanks for your input @qiuwei. You are absolutely right. In your code each browser tab creates a new jp.WebPage through the builder function event_demo. NiceGUI only serves a single instance to all clients. We have to think a bit about this. Stay tuned.

@rodja
Copy link
Member

rodja commented Jan 15, 2022

The architecture of NiceGUI is build around the concept of shared pages. Otherwise you would need to mange the state of each client separately. As you did by attaching the counter to the div-tag (d.count = 0). We have the feeling that it will get too complicated to handle in more complex scenarios. Therefore we would like to stick to shared pages. But we just released a version 0.7.1 of NiceGUI which has a new feature called ui.open. With this you can create a button which generates a new page on click:

#!/usr/bin/env python3
from starlette.websockets import WebSocket
from uuid import uuid4
from nicegui import ui
from nicegui.elements.page import Page

class PrivatePage(Page):

    def __init__(self, socket: WebSocket):
        route = f'/{str(uuid4())}'
        super().__init__(route)
        self.count = 0
        with self:
            self.status = ui.label('0').style('font-size:4em')
            with ui.row():
                ui.button('increment', on_click=self.increment)
                ui.button('CLOSE', on_click=lambda e: ui.open('/', e.socket))
        ui.open(route, socket)

    def increment(self):
        self.count += 1
        self.status.set_text(self.count)

ui.button('open private page', on_click=lambda e: PrivatePage(e.socket))

ui.run()

As you can see every private page has its own state. Please let us know if this is sufficient to solve your problem.

@rodja
Copy link
Member

rodja commented Feb 5, 2022

I'll close the issue. @qiuwei feel free to contact us any time if you still have questions.

@rodja rodja closed this as completed Feb 5, 2022
@me21
Copy link
Contributor

me21 commented Aug 25, 2022

I've come up with my own solution not requiring additional paths. I'll share it a little later. I didn't test it thoroughly though, but the tabs in one browser share the state while the tabs in different browsers are separate.

@falkoschindler
Copy link
Contributor

Interesting! We're also thinking about how NiceGUI creates a common page instance for all clients and how we could (optionally) generate a new one for each client. But it's still just an idea.

@me21
Copy link
Contributor

me21 commented Aug 26, 2022

My solution:

# Creating a PrivatePage for some route will make page instances with different sessions independent.
class PrivatePage(ui.page):

    def __init__(self, route: str, setup_ui: Callable, *args, **kwargs):
        self.setup_ui, self.args, self.kwargs = setup_ui, args, kwargs
        if 'on_disconnect' in kwargs:
            del kwargs['on_disconnect']
        super().__init__(route, *args, **kwargs)
        self.individual_page_viewers: dict[str, int] = {}
        self.individual_pages: dict[str, ui.page] = {}

    async def on_disconnect(self, websocket: WebSocket):
        if 'on_disconnect' in self.kwargs:
            disconnect_handler = self.kwargs['on_disconnect']
            await disconnect_handler() if is_coroutine(disconnect_handler) else disconnect_handler()
        session_id = websocket.cookies['jp_token'].split('.')[0]
        self.individual_page_viewers[session_id] -= 1
        print(f'Viewer quit, session id {session_id}, total {self.individual_page_viewers[session_id]}')
        if self.individual_page_viewers[session_id] == 0:
            self.individual_pages.pop(session_id).remove_page()  # To reclaim used memory

    #TODO: override __enter__ and __exit__ to be able to use it in context manager like ordinary page?
    async def _route_function(self, request: Request):
        # we spawn additional page instance for each session id
        if request.session_id not in self.individual_pages:
            self.individual_pages[request.session_id] = ui.page(self.route,
                                                                *self.args,
                                                                on_disconnect=self.on_disconnect,
                                                                **self.kwargs)
            jp.Route.instances.pop(0)
            self.individual_page_viewers[request.session_id] = 0
            self.setup_ui(self.individual_pages[request.session_id])
        self.individual_page_viewers[request.session_id] += 1
        print(f'New viewer, session_id {request.session_id}, viewers {self.individual_page_viewers[request.session_id]}')
        return await self.individual_pages[request.session_id]._route_function(request)

I also changed on_disconnect function in page.py to call user supplied handler with websocket argument:

    async def on_disconnect(self, websocket=None) -> None:
        for disconnect_handler in ([self.disconnect_handler] if self.disconnect_handler else []) + disconnect_handlers:
            arg_count = len(inspect.signature(disconnect_handler).parameters)
            is_coro = is_coroutine(disconnect_handler)
            if arg_count == 1:
                await disconnect_handler(websocket) if is_coro else disconnect_handler(websocket)
            elif arg_count == 0:
                await disconnect_handler() if is_coro else disconnect_handler()
            else:
                raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
        await super().on_disconnect(websocket)

Usage:

def login(args):
    # you can access the page object as sender.page, etc.
    sender = args.sender
    socket = args.socket
    pass


def setup_login_ui(page: ui.page):
    with page:
        ui.input('User Name').classes('w-full')
        ui.input('Password').classes('w-full').props('type=password')
        ui.button('Log in', on_click=login)


login_page = PrivatePage('/login', setup_ui=setup_login_ui)

ui.run()

Currently it doesn't allow setting up page right after creation like with ordinary pages, it needs a callback which is invoked once per specific session id.
It makes different sessions independent from each other, but tabs in the same browser share the same session, thus still sharing the state. I think it's possible to make them independent too by comparing the websocket instances, not session ids. Or not, because in _route_function we don't have access to websocket.
I didn't test it thoroughly, beware of unknown bugs.

@falkoschindler
Copy link
Contributor

Thanks, @me21! I looked it through and it seems to be a very reasonable approach. I'll probably try to bake it more into the original ui.page. If a regular page would accept a callable (setup_ui in your case), it could act as your PrivatePage automatically. Here is a discussion about this idea: #61

By the way: Providing an optional socket in page.on_disconnect seems very handy and consistent with the other callbacks. I added it to the main branch. 56ff679

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

No branches or pull requests

4 participants