Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Options to build your own UI component

custom_ui_components.livemd

Options to build your own UI component

Mix.install([
  {:kino, "~> 0.13.2"},
  {:jason, "~> 1.4"}
])

The Kino.HTML built-in module

Kino.HTML.new("""

Look!

I wrote this HTML from Kino!

"""
)
Kino.HTML.new("""

  #button {
    width: 5em;
    transition: width 0.5s ease;
    font-size: 1em;
  }


Click


  const button = document.querySelector("#button");

  button.addEventListener("click", (event) => {
    button.textContent = "Clicked!";
    button.style.width = "18em";
  });

""")

Kino.HTML + CSS

defmodule KinoSpinner do
  def new(dimensions \\ "30px") do
    Kino.HTML.new("""
    

    
      .loader {
        border: 16px solid #f3f3f3; /* Light grey */
        border-top: 16px solid #3498db; /* Blue */
        border-radius: 50%;
        width: #{dimensions};
        height: #{dimensions};
        animation: spin 2s linear infinite;
      }

      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
    
    """)
  end
end
KinoSpinner.new()
import Kino.Shorts

How we can use that KinoSpinner with a form:

form =
  Kino.Control.form(
    [
      name: Kino.Input.text("Data", default: "some data to process")
    ],
    submit: "Submit"
  )

output_frame = frame()

Kino.listen(form, fn _event ->
  Kino.Frame.render(output_frame, grid([text("Processing..."), KinoSpinner.new()]))
  Process.sleep(2_000)
  Kino.Frame.render(output_frame, "Processing is done. ✅")
end)

grid([form, output_frame])

Kino.HTML + CSS + Javascript

defmodule KinoTextWithClipboard do
  def new(text) do
    Kino.HTML.new("""
      
        .container {
            box-sizing: border-box;
            position: relative;
            width: 100%;
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 10px;
        }
        .text-content {
            width: 100%;
            margin-bottom: 10px;
            word-wrap: break-word;
        }
        .clipboard-icon {
            position: absolute;
            right: 10px;
            top: 10px;
            cursor: pointer;
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 5px;
        }
        .clipboard-icon:hover {
            background-color: #f0f0f0;
        }
        .copy-feedback {
            position: absolute;
            right: 40px;
            top: 13px;
            background-color: #4CAF50;
            color: white;
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 12px;
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
        }
      

      
        
            #{text}
        
        
            
                
                
            
        
        Copied
      

      
        function copyToClipboard() {
            const textContent = document.getElementById('textContent');
            const textToCopy = textContent.innerText;

            const tempTextArea = document.createElement('textarea');
            tempTextArea.value = textToCopy;
            document.body.appendChild(tempTextArea);

            tempTextArea.select();
            document.execCommand('copy');

            document.body.removeChild(tempTextArea);

            const icon = document.querySelector('.clipboard-icon');
            const feedback = document.getElementById('copyFeedback');

            icon.style.backgroundColor = '#4CAF50';
            feedback.style.opacity = '1';

            setTimeout(() => {
                icon.style.backgroundColor = '';
                feedback.style.opacity = '0';
            }, 2000);
        }
    
    """)
  end
end
Kino.Text.new("some text")
KinoTextWithClipboard.new("some text")

Custom Kino

defmodule KinoJsonInput do
  use Kino.JS
  use Kino.JS.Live

  def new(json) do
    Kino.JS.Live.new(__MODULE__, json)
  end

  def read(kino) do
    Kino.JS.Live.call(kino, :read)
  end

  @impl true
  def init(json, ctx) do
    {:ok, assign(ctx, json: json)}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, ctx.assigns.json, ctx}
  end

  @impl true
  def handle_event("update_json", json, ctx) do
    {:noreply, assign(ctx, json: json)}
  end

  @impl true
  def handle_call(:read, _from, ctx) do
    {:reply, ctx.assigns.json, ctx}
  end

  asset "main.js" do
    """
    export async function init(ctx, json) {
      await ctx.importCSS("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css");
      await ctx.importJS("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js");
      await ctx.importJS("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js");

      await ctx.importCSS("https://cdn.jsdelivr.net/gh/WebCoder49/code-input@2.2/code-input.min.css");
      await ctx.importJS("https://cdn.jsdelivr.net/gh/WebCoder49/code-input@2.2/code-input.min.js");
      await ctx.importJS("https://cdn.jsdelivr.net/gh/WebCoder49/code-input@2.2.1/plugins/indent.min.js");


      // This is needed because the CodeInput lib is activated
      // on window.load (https://github.com/WebCoder49/code-input/blob/v2.2.1/code-input.js#L983-L985)
      // but by the time this JS code is executed, the original load event has already been fired.
      window.dispatchEvent(new Event("load"));


      codeInput.registerTemplate("syntax-highlighted", codeInput.templates.prism(Prism, [new codeInput.plugins.Indent()]));

      ctx.root.innerHTML = `
        ${json}
      `;

      const codeInputEl = document.getElementById("input-json");

      codeInputEl.addEventListener("change", (event) => {
        ctx.pushEvent("update_json", event.target.value);
      });
    }
    """
  end
end
Kino.Input.textarea("Json", default: """
{
  "name": "Hugo Baraúna",
  "age": 18,
  "company": "Dashbit / Livebook"
}
""")
kino_json_input =
  KinoJsonInput.new("""
  {
    "name": "Hugo Baraúna",
    "age": 18,
    "company": "Dashbit / Livebook"
  }
  """)
kino_json_input
|> KinoJsonInput.read()
|> Jason.decode!()