2025, Oct 16 06:33

डायनेमिक Python Shiny UI में प्रति‑बटन remove इवेंट कैसे बाँधें

Python Shiny में डायनेमिक UI के लिए हर X बटन क्लिक को अलग इवेंट से बाँधना सीखें: कोड उदाहरण, प्रति-बटन remove हैंडलर, insert_ui/remove_ui और ID सावधानियाँ.

Python Shiny में डायनेमिक UI तब तक आसान लगता है, जब तक आपको हर तत्व के लिए अलग‑अलग क्रियाएँ नहीं चाहिए। एक ग्लोबल “Add” बटन तो सीधा है, लेकिन प्रत्येक हटाने वाले नियंत्रण को अपना अलग इवेंट ट्रिगर करना चाहिए। अक्सर दिक्कत यहीं आती है: हर डायनेमिक “X” बटन की क्लिक को ऐसे जोड़ना कि वह सिर्फ अपना ही एलिमेंट हटाए और बाकी पर असर न पड़े।

समस्या सेटअप

उद्देश्य एक “Add Square” बटन से चौकोर जैसे विजेट जोड़ना और हर चौकोर को उसके कोने के “X” से हटाना है। जोड़ना तो काम करता है; मुश्किल यह है कि हर अलग “X” पर क्लिक को कैसे पकड़ा जाए। नीचे दिया गया छोटा उदाहरण इसी समस्या को दिखाता है।

from shiny import App, ui, reactive
# यूआई
app_view = ui.page_fluid(
    ui.input_action_button("spawn_tile", "Add Square", class_="btn btn-primary"),
    ui.div(id="tile_area", class_="wrap"),
    ui.tags.style(
        """
        .tile {
            width: 120px;
            height: 120px;
            background-color: #f9f9f9;
            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;
        }
        .wrap {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-top: 20px;
            justify-content: center;
        }
        .rm-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;
        }
        .rm-btn:hover {
            background-color: #e60000;
        }
        """
    ),
)
# सर्वर
def backend(inputs, outputs, sess):
    ids_store = reactive.Value([])
    @reactive.Effect
    @reactive.event(inputs.spawn_tile)
    def make_tile():
        current = ids_store.get()
        new_key = max(current, default=0) + 1
        current.append(new_key)
        ids_store.set(current)
        ui.insert_ui(
            ui=ui.div(
                ui.TagList(
                    ui.input_text(f"label_{new_key}", None, placeholder=f"Square {new_key}"),
                    ui.input_action_button(f"rm_{new_key}", "X", class_="rm-btn"),
                ),
                id=f"tile_{new_key}",
                class_="tile",
            ),
            selector="#tile_area",
            where="beforeEnd",
        )
    @reactive.effect
    def watch_rm():
        keys = ids_store.get()
        for k in keys:
            if inputs.get(f"rm_{k}", 0) > 0:
                ui.remove_ui(selector=f"#tile_{k}")
                remaining = [x for x in keys if x != k]
                ids_store.set(remaining)
                sess.reset_input(f"rm_{k}")
                break
app = App(app_view, backend)

असल में हो क्या रहा है

हर “X” डायनेमिक रूप से बनता है, लेकिन क्लिक पकड़ने की मौजूदा लॉजिक सूची पर घूमकर काउंटर्स देखती है। यह पैटर्न प्रति‑बटन अलग इवेंट स्रोत नहीं बाँधता। किसी खास डायनेमिक बटन की क्लिक पर भरोसेमंद ढंग से प्रतिक्रिया देने के लिए, उस इनपुट के बनते ही उसके लिए इवेंट को स्पष्ट रूप से रजिस्टर करना पड़ता है।

समाधान: बटन बनते समय प्रति‑बटन reactive इवेंट बाँधें

सरल तरीका यह है कि हर नए बने “X” के लिए reactive.event(getattr(input, f"…")) के साथ एक नई reactive फ़ंक्शन रजिस्टर करें। इससे हैंडलर उसी बटन के इनपुट से बंध जाता है, और “X” पर क्लिक करते ही उसका अपना एलिमेंट हट जाता है।

from shiny import App, ui, reactive
app_view = ui.page_fluid(
    ui.input_action_button("spawn_tile", "Add Square", class_="btn btn-primary"),
    ui.div(id="tile_area", class_="wrap"),
    ui.tags.style(
        """
        .tile { width: 120px; height: 120px; background-color: #f9f9f9; 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; }
        .wrap { display: flex; flex-wrap: wrap; gap: 15px; margin-top: 20px; justify-content: center; }
        .rm-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; }
        .rm-btn:hover { background-color: #e60000; }
        """
    ),
)
def backend(inputs, outputs, sess):
    ids_store = reactive.Value([])
    @reactive.Effect
    @reactive.event(inputs.spawn_tile)
    def make_tile():
        current = ids_store.get()
        new_key = max(current, default=0) + 1
        current.append(new_key)
        ids_store.set(current)
        ui.insert_ui(
            ui=ui.div(
                ui.TagList(
                    ui.input_text(f"label_{new_key}", None, placeholder=f"Square {new_key}"),
                    ui.input_action_button(f"rm_{new_key}", "X", class_="rm-btn"),
                ),
                id=f"tile_{new_key}",
                class_="tile",
            ),
            selector="#tile_area",
            where="beforeEnd",
        )
        @reactive.effect
        @reactive.event(getattr(inputs, f"rm_{new_key}"))
        def handle_remove():
            ui.remove_ui(selector=f"#tile_{new_key}")
app = App(app_view, backend)

यह पैटर्न तत्व जोड़ते ही हर बटन के लिए अलग reactive effect बना देता है, जिससे उसी बटन पर क्लिक सही तरह से observe होता है।

यह क्यों मायने रखता है

जब UI रनटाइम पर बनता है, तो इनपुट शुरू में मौजूद नहीं होते। इवेंट बाइंडिंग उसी समय करनी होती है जब तत्व बनता है। अगर हर डायनेमिक इनपुट के लिए इवेंट स्पष्ट रूप से दर्ज नहीं करेंगे, तो क्लिक हैंडलिंग नाज़ुक और उलझी हुई हो जाती है। प्रति‑बटन reactive.event बाँधने से लॉजिक स्थानीय और अनुमानित रहता है।

आईडी के बारे में एक सावधानी

ऊपर बताए तरीके में एक व्यावहारिक बात का ध्यान रखें: अगर सबसे बड़ी ID वाला तत्व हटा दिया गया और बाद में वही ID फिर से उपयोग हो गई, तो उसी ID के लिए वही हैंडलर नाम दोबारा बन सकता है, जिससे समस्याएँ पैदा होंगी। इसलिए IDs कैसे बनती और दोबारा उपयोग होती हैं, इसे संभालकर रखें।

मुख्य बातें

Python Shiny में अलग‑अलग हटाए जा सकने वाले डायनेमिक तत्वों के लिए, तत्व रेंडर होते ही प्रति‑तत्व reactive हैंडलर जोड़ें। सही बटन से बाइंड करने के लिए reactive.event(getattr(input, f"…")) का उपयोग करें। संभावित ID पुन: उपयोग के प्रति सजग रहें, क्योंकि उसी पहचानकर्ता के लिए हैंडलर को फिर से बनाना अनपेक्षित व्यवहार ला सकता है।

यह लेख StackOverflow पर एक प्रश्न (लेखक: Goldjunge3010) और furas के उत्तर पर आधारित है।