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

[feature request] improvements to triggering events (such as a button click) #1418

Open
sebovzeoueb opened this issue May 23, 2024 · 0 comments

Comments

@sebovzeoueb
Copy link

I think it would be nice to be able to raise an input event without incrementing a number. To start with it's not an intuitive system, and it would allow for passing values with the event.

It would make lists of items simpler to implement and I think being able to raise arbitrary events from code would be a good feature. It would differ from the current reactive value method in that you could trigger an event for the same value multiple times, currently a change in value is required to trigger a reactive update.

Consider the following test app I made while getting help on Discord:

from shiny import module, reactive, ui, render, Inputs, Outputs, Session, App
from shiny._utils import rand_hex

# ---
# MOCK DATABASE
# ---

# imagine this is a database query result
items = [
    "item_1",
    "item_2",
    "item_3"
]

# this is a function that runs the query
def get_items():
    return items

# this function deletes items based on the query
def delete_item(index):
    del items[index]

# ---
# ITEM COMPONENT
# ---

@module.ui
def item_ui(item):
    return [
        ui.markdown(item),
        ui.input_action_button("delete_item", "Delete")
    ]

@module.server
def item_server(input: Inputs, output: Outputs, session: Session, index, on_delete_callback):

    @reactive.effect
    @reactive.event(input.delete_item, ignore_init=True)
    def on_delete():
        delete_item(index)
        on_delete_callback()

# ---
# MAIN
# ---

app_ui = ui.page_auto(
    ui.output_ui("item_list"),
    ui.input_action_button("refresh", "Refresh")
)

def app_server(input: Inputs, output: Outputs, session: Session):

    items = reactive.value([])
    delete_trigger = reactive.value(0)

    def on_delete_callback():
        delete_trigger.set(delete_trigger.get() + 1)

    # this needs to trigger when an item is deleted
    # currently we can manually trigger to show that backend is working as intended
    @reactive.effect
    @reactive.event(input.refresh, delete_trigger, ignore_none=False)
    def on_change_items():
        # we create a random ID to sync UI and server for each item
        items.set([{"item": item, "id": rand_hex(4)} for item in get_items()])

    @reactive.effect
    def item_servers():
        for index, item in enumerate(items.get()):
            item_server(item["id"], index, on_delete_callback)

    @render.ui
    def item_list():
        return [item_ui(item["id"], item["item"]) for item in items.get()]
    
app = App(app_ui, app_server)

Handling the list of items is needlessly unwieldy here. With my proposal you could do this:

from shiny import reactive, ui, render, Inputs, Outputs, Session, App
from shiny._utils import rand_hex

# ---
# MOCK DATABASE
# ---

# imagine this is a database query result
items = [
    "item_1",
    "item_2",
    "item_3"
]

# this is a function that runs the query
def get_items():
    return items

# this function deletes items based on the query
def delete_item(index):
    del items[index]

# ---
# MAIN
# ---

app_ui = ui.page_auto(
    ui.output_ui("item_list"),
    ui.input_action_button("refresh", "Refresh")
)

def app_server(input: Inputs, output: Outputs, session: Session):

    items = reactive.value([])

    # this needs to trigger when an item is deleted
    # currently we can manually trigger to show that backend is working as intended
    @reactive.effect
    @reactive.event(input.refresh, input.delete_item, ignore_none=False)
    def on_change_items():
        # we create a random ID to sync UI and server for each item
        items.set([{"item": item, "id": rand_hex(4)} for item in get_items()])

    @render.ui
    def item_list():
        return [[
            ui.markdown(item["item"]),
            ui.input_action_button("delete_item", "Delete", value=index)  
        ] for index, item in enumerate(items.get())]
    
    @reactive.effect
    @reactive.event(input.delete_item, ignore_init=True)
    def on_delete():
        # the input will be set to the index to delete by the button click
        delete_item(input.delete_item())
    
app = App(app_ui, app_server)

quite a lot less code and all contained within the UI and server.

In addition to being able to pass values to button events, you could add a function like raise_event("delete_item", i) to be able to trigger on_delete from somewhere else in the server code without needing a reactive.value

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

No branches or pull requests

1 participant