Repository: Appsilon/py_shiny_semantic (language: python)

Issue:
[Elements] Button



Discussion / hints:

Fix patch:
diff --git a/app.py b/app.py
index 5a1a752..2b7413e 100644
--- a/app.py
+++ b/app.py
@@ -1,6 +1,9 @@
-from shiny import App, ui
+from random import randint
+
+from shiny import App, reactive, render, ui
 
 from shiny_semantic import page_semantic
+from shiny_semantic.elements import button, update_button
 
 app_ui = page_semantic(
     ui.tags.div(
@@ -14,11 +17,104 @@
         ),
         class_="ui inverted vertical masthead center aligned segment",
     ),
+    ui.tags.div(
+        ui.tags.h2("Button", class_="ui centered header"),
+        ui.tags.h3("Shiny Bound Inputs", class_="ui header"),
+        button("button", "Click me"),
+        ui.tags.span(
+            "Clicks:",
+            ui.output_text("n_clicks", inline=True),
+            class_="item",
+        ),
+        button(
+            "update",
+            "Update other btn's label",
+            class_name="right floated",
+            icon_name="arrow left",
+        ),
+        ui.tags.h3("Button Types", class_="ui header"),
+        ui.tags.div(
+            button("btn1", "Default"),
+            button("btn2", "Primary", class_name="primary"),
+            button("btn3", "Secondary", class_name="secondary"),
+            button("btn4", "Basic", class_name="basic"),
+            button("btn5", "Tertiary", class_name="tertiary"),
+            button("btn6", "Icon", icon_name="pause"),
+            class_="ui container",
+        ),
+        ui.tags.h3("Button Colors", class_="ui header"),
+        ui.tags.div(
+            button("btn7", "Positive", class_name="positive"),
+            button("btn8", "Negative", class_name="negative"),
+            button("btn9", "Purple", class_name="purple"),
+            button("btn10", "Olive", class_name="olive"),
+            button("btn11", "Orange", class_name="basic orange"),
+            button("btn12", "Teal", class_name="tertiary teal"),
+            class_="ui container",
+        ),
+        ui.tags.h3("Button Sizes", class_="ui header"),
+        ui.tags.div(
+            button("btn13", "Mini", class_name="mini"),
+            button("btn14", "Tiny", class_name="tiny"),
+            button("btn15", "Small", class_name="small"),
+            button("btn16", "Medium", class_name="medium"),
+            button("btn17", "Large", class_name="large"),
+            button("btn18", "Big", class_name="big"),
+            class_="ui container",
+        ),
+        ui.tags.h3("Button Social", class_="ui header"),
+        ui.tags.div(
+            button(
+                "btn19",
+                "Facebook",
+                class_name="facebook",
+                icon_name="facebook",
+            ),
+            button(
+                "btn20",
+                "Linkedin",
+                class_name="linkedin",
+                icon_name="linkedin",
+            ),
+            button(
+                "btn21",
+                "Instagram",
+                class_name="instagram",
+                icon_name="instagram",
+            ),
+            button(
+                "btn22",
+                class_name="circular whatsapp icon",
+                icon_name="whatsapp",
+            ),
+            button(
+                "btn23",
+                class_name="circular telegram icon",
+                icon_name="telegram",
+            ),
+            button(
+                "btn24",
+                class_name="circular twitter icon",
+                icon_name="twitter",
+            ),
+            class_="ui container",
+        ),
+        class_="ui raised padded container segment",
+    ),
+    title="Example: Buttons",
 )
 
 
-def app_server(input, output, session):
-    pass
+def server(input, output, session):
+    @output(id="n_clicks")
+    @render.text
+    def _():
+        return input.button()
+
+    @reactive.Effect
+    @reactive.event(input.update)
+    def _():
+        update_button("button", label=str(randint(0, 10)))
 
 
-app = App(app_ui, app_server)
+app = App(app_ui, server)
diff --git a/shiny_semantic/elements/__init__.py b/shiny_semantic/elements/__init__.py
index 8c7a6da..fbce1b3 100644
--- a/shiny_semantic/elements/__init__.py
+++ b/shiny_semantic/elements/__init__.py
@@ -1,1 +1,7 @@
 # Semantic `Elements` section, e.g. Button, Flag, Icon, etc
+__all__ = (
+    "button",
+    "update_button",
+)
+
+from .button import button, update_button
diff --git a/shiny_semantic/elements/button.py b/shiny_semantic/elements/button.py
new file mode 100644
index 0000000..52e7e94
--- /dev/null
+++ b/shiny_semantic/elements/button.py
@@ -0,0 +1,43 @@
+from typing import Optional
+
+from shiny.session import Session, require_active_session
+from shiny.ui import tags
+
+
+def button(
+    input_id: str,
+    label: Optional[str] = None,
+    *args,
+    icon_name: Optional[str] = None,
+    class_name: Optional[str] = None,
+):
+    icon = icon_name and tags.i(class_=f"{icon_name} icon")
+    class_ = f"ui {class_name or ''} button"
+
+    return tags.button(
+        icon,
+        label,
+        *args,
+        id=input_id,
+        class_=class_,
+    )
+
+
+def update_button(
+    input_id: str,
+    *,
+    label: Optional[str] = None,
+    icon_name: Optional[str] = None,
+    session: Optional[Session] = None,
+):
+    session = require_active_session(session)
+    msg = {
+        "label": label,
+        "icon": icon_name,
+    }
+    session.send_input_message(input_id, _drop_none(msg))
+
+
+# Credits: shiny package by Posit
+def _drop_none(x) -> dict[str, object]:
+    return {k: v for k, v in x.items() if v is not None}
diff --git a/shiny_semantic/www/shiny-semantic-bindings.js b/shiny_semantic/www/shiny-semantic-bindings.js
index 7c383e5..549a5a3 100644
--- a/shiny_semantic/www/shiny-semantic-bindings.js
+++ b/shiny_semantic/www/shiny-semantic-bindings.js
@@ -1,4 +1,68 @@
 /**
  * All shiny <-> semantic bindings in one file
  * to be loaded when the app first loads
+ *
+ * The code below was originally developed by Appsilon team for `shiny.semantic` R package
+ * https://github.com/Appsilon/shiny.semantic
  */
+
+var semanticButtonBinding = new Shiny.InputBinding();
+$.extend(semanticButtonBinding, {
+  find: function (scope) {
+    return $(scope).find(".button");
+  },
+  getValue: function (el) {
+    return $(el).data("val") || 0;
+  },
+  setValue: function (el, value) {
+    $(el).data("val", value);
+  },
+  getType: function (el) {
+    return "shiny.action";
+  },
+  subscribe: function (el, callback) {
+    $(el).on("click.semanticButtonBinding", function (e) {
+      var $el = $(this);
+      var val = $el.data("val") || 0;
+      $el.data("val", val + 1);
+
+      callback();
+    });
+  },
+  getState: function (el) {
+    return { value: this.getValue(el) };
+  },
+  receiveMessage: function (el, data) {
+    var $el = $(el);
+    // retrieve current label and icon
+    var label = $el.text();
+    var icon = "";
+
+    // to check (and store) the previous icon, we look for a $el child
+    // object that has an i tag, and some (any) class (this prevents
+    // italicized text - which has an i tag but, usually, no class -
+    // from being mistakenly selected)
+    if ($el.find("i[class]").length > 0) {
+      var icon_html = $el.find("i[class]")[0];
+      if (icon_html === $el.children()[0]) {
+        // another check for robustness
+        icon = $(icon_html).prop("outerHTML");
+      }
+    }
+
+    // update the requested properties
+    if (data.hasOwnProperty("label")) label = data.label;
+    if (data.hasOwnProperty("icon")) {
+      icon = data.icon;
+      // if the user entered icon=character(0), remove the icon
+      if (icon.length === 0) icon = "";
+    }
+
+    // produce new html
+    $el.html(icon + " " + label);
+  },
+  unsubscribe: function (el) {
+    $(el).off(".semanticButtonBinding");
+  },
+});
+Shiny.inputBindings.register(semanticButtonBinding, "shiny.semanticButton");


Test patch:
diff --git a/tests/test_button.py b/tests/test_button.py
new file mode 100644
index 0000000..f29b823
--- /dev/null
+++ b/tests/test_button.py
@@ -0,0 +1,32 @@
+import unittest
+
+from shiny import App
+
+from shiny_semantic import page_semantic
+from shiny_semantic.elements import button
+
+
+class TestButton(unittest.TestCase):
+    def test_semantic_button(self):
+        test_case_1 = '<button id="test1" class="ui  button">'
+        test_case_2 = '<button id="test2" class="ui red basic button">'
+        test_case_3 = '<i class="plane icon">'
+
+        app = App(
+            ui=page_semantic(
+                button("test1", "Test1"),
+                button("test2", "Test2", class_name="red basic"),
+                button("test3", "Test3", icon_name="plane"),
+            ),
+            server=None,
+        )
+
+        html = app.ui.get("html")
+
+        self.assertTrue(test_case_1 in html)
+        self.assertTrue(test_case_2 in html)
+        self.assertTrue(test_case_3 in html)
+
+
+if __name__ == "__main__":
+    unittest.main()