posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.32k stars 82 forks source link

Unable to build form with submit button from HTML #1770

Open thohan88 opened 1 week ago

thohan88 commented 1 week ago

This may be intentional as I seem to remember there being some issues with submit buttons in shiny for R.

My use case was just a simple form POST with action attached, and I expected this to work:

from shiny import App, ui
from htmltools import HTML

app_ui = ui.page_auto(
    ui.h2("Login"),
    HTML(
        """
         <form action="/api/auth/web/login" method="POST">
            <input type="email" id="username" name="username">
            <input type="password" id="password" name="password">
            <button type="submit" class="btn btn-primary">Login</button>
        </form>
        """
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)

Nothing happens when the button is clicked, and I can't find the cause. A pure HTML file seems to work fine:

<!DOCTYPE html>
<html>
    <body>
        <form action="/api/auth/web/login" method="POST">
            <input type="email" id="username" name="username">
            <input type="password" id="password" name="password">
            <button type="submit" class="btn btn-primary">Login</button>
        </form>
    </body>
</html>
thohan88 commented 1 week ago

After looking further into this, I realize this may be the intended behavior: https://github.com/rstudio/shiny/blob/501b012b2b82c0c468dc921ff0a3d9a682c20623/srcts/src/shiny/index.ts#L179-L192

For anyone stumbling on this, you can override the "inputsDefer"-behavior for submit buttons with some javascript:

document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('loginSubmit').addEventListener('click', function(event) {
        document.getElementById('loginForm').submit();
    });
});

So the following works:

from shiny import App, ui
from htmltools import HTML

# JavaScript to handle form submission via button click
# Overrides https://github.com/rstudio/shiny/blob/501b012b2b82c0c468dc921ff0a3d9a682c20623/srcts/src/shiny/index.ts#L179-L192
js = """
document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('loginSubmit').addEventListener('click', function(event) {
        document.getElementById('loginForm').submit();
    });
});
"""

app_ui = ui.page_auto(
    ui.h2("Login"),
    ui.head_content(ui.tags.script(js)),
    HTML(
        """
        <form action="/api/auth/web/login" method="POST" id="loginForm">
            <input type="email" id="username" name="username">
            <input type="password" id="password" name="password">
            <button type="button" class="btn btn-primary" id="loginSubmit">Login</button>
        </form>
        """
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)

I am currently experimenting with combining FastAPI and Shiny. In my specific case, the FastAPI endpoint /api/auth/web/login sets a cookie upon successful authentication which gates access to another shiny app. This simple login app could of course be a static page, but I tried to quickly mock something up with shiny. Curious to hear if anyone has advice on a better approach. Otherwise, feel free to close this issue.