DEV Community

Aaron Gong
Aaron Gong

Posted on • Updated on

Streamlit Custom Components + Vite + Vanilla JS

Create Component Based On vanilla JS

  • From the template folder
  • Create new component using vite and add init.py code for testing the component
$ mkdir vite_vanilla_component
$ cd vite_vanilla_component
$ npm init vite@latest frontend --template vanilla # npm v6 (v7 is different)
$ touch __init__.py # command may be different in Windows
Enter fullscreen mode Exit fullscreen mode
  • Add in the init.py code below
import os
import streamlit.components.v1 as components

_RELEASE = False

if not _RELEASE:
  _component_func = components.declare_component(
    "vite_vanilla_component",
    url="http://localhost:3000", # vite dev server port
  )
else:
  parent_dir = os.path.dirname(os.path.abspath(__file__))
  build_dir = os.path.join(parent_dir, "frontend/dist")
  _component_func = components.declare_component("vite_vanilla_component", path=build_dir)

def my_component(name, key=None):
  component_value = _component_func(name=name, key=key, default=0)
  return component_value

if not _RELEASE:
  import streamlit as st
  st.subheader("Component Test")
  num_clicks = my_component(name = "NameViteVanilla")
  st.markdown("You've clicked %s times!" % int(num_clicks))
Enter fullscreen mode Exit fullscreen mode
  • install the frontend node libraries, streamlit-component-lib, create vite.config.js
$ cd frontend
$ npm i
$ npm i streamlit-component-lib
$ touch vite.config.js
Enter fullscreen mode Exit fullscreen mode
  • vite.config.js should look like below
export default {
  base: './'
}
Enter fullscreen mode Exit fullscreen mode
  • replace the content of main.js with the following (based on the reactless-template of the original component-template repo)
import { Streamlit } from "streamlit-component-lib"

const span = document.body.appendChild(document.createElement("span"))
const textNode = span.appendChild(document.createTextNode(""))
const button = span.appendChild(document.createElement("button"))
button.textContent = "Click Me!"

let numClicks = 0
let isFocused = false
button.onclick = function() {
  numClicks += 1
  Streamlit.setComponentValue(numClicks)
}

button.onfocus = function() { isFocused = true }
button.onblur = function() { isFocused = false }

function onRender(event) {
  const data = event.detail
  if (data.theme) {
    const borderStyling = `1px solid var(${
      isFocused ? "--primary-color" : "gray"
    })`
    button.style.border = borderStyling
    button.style.outline = borderStyling
  }
  button.disabled = data.disabled
  let name = data.args["name"]
  textNode.textContent = `Hello, ${name}! ` + String.fromCharCode(160)
  Streamlit.setFrameHeight()
}

Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, onRender)
Streamlit.setComponentReady()
Streamlit.setFrameHeight()
Enter fullscreen mode Exit fullscreen mode

Running the example

From the base directory, navigate to the frontend and serve it from a dev server:

$ cd template/vite_vanilla_component/frontend
$ npm run dev
Enter fullscreen mode Exit fullscreen mode

On a separate terminal, from base directory, navigate to and run the Streamlit app (assuming python environment has been activated):

$ cd template
$ streamlit run vite_vanilla_component/__init__.py
Enter fullscreen mode Exit fullscreen mode

You should see the image below, and clicking the button should increment the count.

image

The component looks truncated. To fix this, set body style in the component to have margin: 0;.

Discussion (0)