Code snippet: create Components without any frontend tooling (no React, Babel, Webpack, etc)

We never properly documented this, but you can actually create Components in a super scrappy way using nothing but a static HTML file!

Here’s how:

mycomponent/index.html

<html>
  <body>
    <!-- Set up your HTML here -->
    <input id="myinput" value="" />

    <script>
      // ----------------------------------------------------
      // Just copy/paste these functions as-is:

      function sendMessageToStreamlitClient(type, data) {
        var outData = Object.assign({
          isStreamlitMessage: true,
          type: type,
        }, data);
        window.parent.postMessage(outData, "*");
      }

      function init() {
        sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1});
      }

      function setFrameHeight(height) {
        sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height});
      }

      // The `data` argument can be any JSON-serializable value.
      function sendDataToPython(data) {
        sendMessageToStreamlitClient("streamlit:setComponentValue", data);
      }

      // ----------------------------------------------------
      // Now modify this part of the code to fit your needs:

      var myInput = document.getElementById("myinput");

      // data is any JSON-serializable value you sent from Python,
      // and it's already deserialized for you.
      function onDataFromPython(event) {
        if (event.data.type !== "streamlit:render") return;
        myInput.value = event.data.args.my_input_value;  // Access values sent from Python here!
      }

      myInput.addEventListener("change", function() {
        sendDataToPython({
          value: myInput.value,
          dataType: "json",
        });
      })

      // Hook things up!
      window.addEventListener("message", onDataFromPython);
      init();

      // Hack to autoset the iframe height.
      window.addEventListener("load", function() {
        window.setTimeout(function() {
          setFrameHeight(document.documentElement.clientHeight)
        }, 0);
      });

      // Optionally, if the automatic height computation fails you, give this component a height manually
      // by commenting out below:
      //setFrameHeight(200);
    </script>
  </body>
</html>

mycomponent/__init__.py

import streamlit.components.v1 as components
mycomponent = components.declare_component(
    "mycomponent",
    path="./mycomponent"
)

And you’re done!


How to use this:

import streamlit as st
from mycomponent import mycomponent
value = mycomponent(my_input_value="hello there")
st.write("Received", value)
34 Likes

5 Likes

Hello! Very interresting!
But I need help!

Unfortunately, I was not able to run this example.
This gets displayed on my app screen:

Capture

The files tree of my tiny project is as follows:

psychroST
… psychroST.py
… psychroST
… … __init__.py
… … chart.py
… … process.py
… mycomponent
… … __init__.py
… … index.html

The two last files are from your post.
My app script (psychroST.py) contains these lines:

from mycomponent import *

value = mycomponent.example(my_input_value=“hello there”)
st.write(“Received”, value)

Thanks for your suggestions …
Is there an online running version of your post?

Michel

Hi Michel (@maajdl),

looks actually fine for me.
Seems only the input field is missing.
Did you try to comment //setFrameHeight(200); out?

Best regards
Chris

2 Likes

Yes, thanks Chris .
That was the problem.
Michel

1 Like

Thanks, @chris_klose !

BTW, I just realized the Markdown interpreter ate the underscores from __init__.py. Fixed it now

2 Likes

Right.
Did the same.
Thanks
Michel

Hi, Thiago! What if I want to fill my index.html with jinja at first and then render it?

If you only need to do that once, then you can just render your jinja template and save to disk as index.html

Otherwise, if you need it to be dynamic, you could wrap your Python component in a function that calls mycomponent(html=my_rendered_jinja_template) to send the rendered jinja HTML to a dummy index.html that just replaces some div’s .innerHTML with it.

(I’m not able to write example code right now, but that’s the gist of it!)

3 Likes

Elegant use of postMessage between iframe child and parent! What an idea!

1 Like

Hello everyone.
I’m trying to add a Mapbox’s geolocator (link) into my streamlit app but I’m struggling to get the json value and send it back to python.
Any advice ?

I’m using this method and it works well. However, I can’t seem to load local videos on my HTML file with proper controls. I’ve tried (Restrict Download of Images & Videos on Streamlit App - #2 by andfanilo) but it does not seem to be able to load from the static folder. Anyone know how to resolve this?

Hi @thiago , that’s very insightful!

I am currently playing with it and one piece I miss is how a state of component can be saved / loaded? I want my component to behave like built-in.

I think in your example component just ignores the changes and gets user input, correct?

oh, I think I realized how that works, streamlit just keeps iframe around until it is not used

Hi @thiago , here’s a rookie asking a few questions:

  1. Is it possible to program a dropdown / selectbox this way, where elements need to be taken from a CSV (or even a small set of elements)? How would I pass them from Streamlit to HTML
  2. I guess that something resembling a form can be done, but with a collection of individual components, right?
  3. How could we add other features to this type of programming. eg. Date Picker?

Wouldn’t entry widgets created this way process faster than if they were done the normal way with Streamlit (because of the reload)? I mean, if your entry screen had a large number of widget… say 40… (along with dependencies), Streamlit would slow down to a crawl, but not if you program it this way, right?

Thanks in advance.

Cheers

Hi. Is there a way to avoid having a separate HTML file, by just providing its content as a string to declare_component instead of the path parameter?

Out of context of this post, but you can load an html string into components.iframe like this:

raw_html = ''' ... some html ... '''
b64_html = base64.b64encode(raw_html).decode()
src = f"data:text/html;base64,{b64_html}"
components.iframe(src=src, width=1100, height=1200, scrolling=True)
1 Like

Ingenius idea! I assume it works anywhere a URL is expected, right?

Yes, just change the mime type (data). I haven’t tested all variations.

For the record, this didn’t work with declare_component. Maybe because it is expected that something is served? :-/

1 Like