pythonuser-interfaceshiny-reactivitypy-shiny

Dynamically adding and removing UI elements


I am trying to dynamically add and remove UI elements to a python shiny application. For adding elements I want to have a generic "Add element" button. For removal however, I want to have an individual "X" button at the corner of each element. Adding elements already works as it should. I am currently struggling with observing the clicks on each individual "X" button.

The code I have currently is this:

from shiny import App, ui, reactive

# Define the UI
app_ui = ui.page_fluid(
    ui.input_action_button("add_square", "Add Square", class_="btn btn-primary"),
    ui.div(id="square_container", class_="container"),
    ui.tags.style(
        """
        .rounded-square {
            width: 120px;
            height: 120px;
            background-color: #f9f9f9; /* Light gray background */
            border-radius: 15%;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 10px;
            box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
            position: relative;
        }
        .container {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-top: 20px;
            justify-content: center;
        }
        .delete-btn {
            position: absolute;
            top: 5px;
            right: 5px;
            background-color: #ff4d4d;
            color: white;
            border: none;
            border-radius: 50%;
            width: 20px;
            height: 20px;
            font-size: 12px;
            cursor: pointer;
            text-align: center;
            line-height: 17px;
            padding: 0;
        }
        .delete-btn:hover {
            background-color: #e60000;
        }
        """
    ),
)


# Define the server logic
def server(input, output, session):
    # Reactive list to store square IDs
    square_ids = reactive.Value([])

    @reactive.Effect
    @reactive.event(input.add_square)
    def add_square():
        """Add a new square dynamically."""
        current_ids = square_ids.get()

        # Generate a new unique ID for the square
        new_id = max(current_ids, default=0) + 1
        current_ids.append(new_id)
        square_ids.set(current_ids)

        # Add a new square dynamically into the DOM
        ui.insert_ui(
            ui=ui.div(
                ui.TagList(
                    ui.input_text(
                        f"text_{new_id}", None, placeholder=f"Square {new_id}"
                    ),
                    ui.input_action_button(
                        f"delete_{new_id}", "X", class_="delete-btn"
                    ),
                ),
                id=f"square_{new_id}",  # Unique ID for the square
                class_="rounded-square",
            ),
            selector="#square_container",  # Add inside the container
            where="beforeEnd",
        )

    @reactive.effect
    def monitor_delete_buttons():
        """Monitor delete button clicks and handle square deletions."""
        current_ids = square_ids.get()

        for square_id in current_ids:
            # Check if the delete button for `square_id` has been clicked
            if input.get(f"delete_{square_id}", 0) > 0:
                # Remove the square from the UI
                ui.remove_ui(selector=f"#square_{square_id}")

                # Update the reactive square list
                updated_ids = [s for s in current_ids if s != square_id]
                square_ids.set(updated_ids)

                # Reset the delete input to avoid duplicate actions
                session.reset_input(f"delete_{square_id}")
                break


# Create the Shiny app
app = App(app_ui, server)

Solution

  • I think: inside add_square() you have to create new function for every button using @reactive.event(getattr(input, f"delete_{new_id}"))

    @reactive.Effect
    @reactive.event(input.add_square)
    def add_square():
        """Add a new square dynamically."""
        current_ids = square_ids.get()
    
        # Generate a new unique ID for the square
        new_id = max(current_ids, default=0) + 1
        current_ids.append(new_id)
        square_ids.set(current_ids)
    
        # Add a new square dynamically into the DOM
        ui.insert_ui(
            ui=ui.div(
                ui.TagList(
                    ui.input_text(
                        f"text_{new_id}", None, placeholder=f"Square {new_id}"
                    ),
                    ui.input_action_button(
                        f"delete_{new_id}", "X", class_="delete-btn"
                    ),
                ),
                id=f"square_{new_id}",  # Unique ID for the square
                class_="rounded-square",
            ),
            selector="#square_container",  # Add inside the container
            where="beforeEnd",
        )
    
        # --- created inside add_square() ---
    
        @reactive.effect
        @reactive.event(getattr(input, f"delete_{new_id}"))
        def delete_button():
            print('delete_button')
            ui.remove_ui(selector=f"#square_{new_id}")
    

    But I don't know how it will behave if you delete button with max ID and create new button which will have the same ID. It will create the same function @reactive.event(getattr(input, f"delete_{new_id}")) second time and it can make problem