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

Hi @thiago, Thanks for this code snippet
I just need the screen width and height to use in my application. This functionality seems to be helping my case.
I changed the code piece in index.html.

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

image
and I am getting this output. I dont want the message to be displayed in text input rather need width and height as a variable in backend python. the value is always None. What should I do for my request. Please guide

1 Like

@Murugan_Yuvaraaj_M_P Are you calling sendDataToPython in your script anywhere? That’s what you’ll need to call in order to send the data back to your python script.

If you’re interested, I created a cookiecutter template for creating a component that uses a slightly modified version of Thiago’s snippet and wrote a blog post showing how to use it How to build your own Streamlit component

1 Like

Thanks for the example. However whenever i add a
<script src='some_external.js' />
I get the following error:

The app is attempting to load the component from ****, and hasn't received its "streamlit:componentReady" message

Would appreciate any help in this. I am including an external js and do not want to paste it verbatim in <script>

In addition to @thiago’s and @blackary’s articles I wrote one which starts from the very basics and progresses towards a full blown React / Next.js component design. You can read up to the end of section “Component Zero”.

3 Likes

@thiago how can I add a simple React component into mycomponent/index.html ? do you have an example? Thanks in advance

UPDATE: please ignore my message, i was able to do it. Thanks again :pray:

hey @thiago, this is really cool, thanks a lot for sharing.
I am trying to have a clickable list of items so wanted to slightly alter your example with the click event and it looks like it somehow triggers twice so the variable is reset. Any idea how to prevent that?
The same is happening to the click detector lib, and it looks like more people have experienced this. Duplication when clicking · Issue #9 · vivien000/st-click-detector · GitHub
this is all i added to your code, and its still working fine with the change event.

       <div id="justadiv">hohoho</div>
        var justadiv = document.getElementById("justadiv");

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

ok, my example is wrong, i had 2 eventlisteners. the issue with double load is when I click on a table generated by pandas - so i guess there are some more events in play.

with plain html everything works fine, thanks :slight_smile:

@thiago or anyone could you give me a hand with my challenge?

@thiago
Thanks you so much for made this code snipset.
I’m trying to custom my own html component to paste image from clipboard to streamlit, I dont know how to modify js functions in your index.html component to get data from clipboard. Can you give me an advice for it ? Thank you.

Hello Everyone
cc: @asehmi @aghasemi @thiago

I deployed my streamlit app with a custom component using this post as an example.

After deploying my streamlit as a microservice in kubernetes, I am getting this error up on the screen

Your app is having trouble loading the idx_db_component.frontend.idxdb_component component.
If this is an installed component that works locally, the app may be having trouble accessing the component frontend assets due to network latency or proxy settings in your app deployment.

I really need urgent help. Please advise.

<!-- See: https://discuss.streamlit.io/t/code-snippet-create-components-without-any-frontend-tooling-no-react-babel-webpack-etc/13064 -->

<style>
  body {
    background-color: lightblue;
    border-color: rgb(246, 51, 102);
    border-width: medium;
    font-family: sans-serif;
    font-size: 16px;
  }

  h1 {
    font-family: sans-serif;
    font-size: 24px;
    font-weight: normal;
    color: #262730;
  }

  #message_div {
    font-family: sans-serif;
    font-size: 12px;
    font-weight: normal;
    color: #262730;
  }
</style>

<html>

<head>
  <script src="./idxdbwrapper.js"></script>
</head>

<body>
  <script>
    const SET_COMPONENT_VALUE = "streamlit:setComponentValue"
    const RENDER = "streamlit:render"
    const COMPONENT_READY = "streamlit:componentReady"
    const SET_FRAME_HEIGHT = "streamlit:setFrameHeight"

    function _sendMessage(type, data) {
      // copy data into object
      var outData = Object.assign({
        isStreamlitMessage: true,
        type: type,
      }, data)

      if (type == SET_COMPONENT_VALUE) {
        console.log("_sendMessage data: " + JSON.stringify(data))
        console.log("_sendMessage outData: " + JSON.stringify(outData))
      }
      window.parent.postMessage(outData, "*")
    }

    function initialize(pipeline) {
      // Hook Streamlit's message events into a simple dispatcher of pipeline handlers
      window.addEventListener("message", (event) => {
        if (event.data.type == RENDER) {
          // The event.data.args dict holds any JSON-serializable value
          // sent from the Streamlit client. It is already deserialized.
          pipeline.forEach(handler => {
            handler(event.data.args)
          })
        }
      })

      _sendMessage(COMPONENT_READY, { apiVersion: 1 });

      // Component should be mounted by Streamlit in an iframe, so try to autoset the iframe height.
      window.addEventListener("load", () => {
        window.setTimeout(function () {
          // setFrameHeight(document.documentElement.clientHeight)
          setFrameHeight(1)
        }, 0)
      })

      // Optionally, if auto-height computation fails, you can manually set it
      // (uncomment below)
      //setFrameHeight(200)
    }

    function setFrameHeight(height) {
      _sendMessage(SET_FRAME_HEIGHT, { height: height })
    }

    // The `data` argument can be any JSON-serializable value.
    function notifyHost(data) {
      _sendMessage(SET_COMPONENT_VALUE, data)
    }

    function sortJsonListByTimestampDesc(jsonList) {
      return jsonList.sort((a, b) => {
        const timestampA = new Date(a.timestamp);
        const timestampB = new Date(b.timestamp);
        return timestampB - timestampA;
      }).slice(0, 20); // Add .slice(0, 10) to get the top 10
    }

    // ----------------------------------------------------
    // Define a pipeline of inbound property handlers
    let isInitialized = false;
    // Set initial value sent from Streamlit!
    function initializeProps_Handler(props) {
      if (!isInitialized) { // Check the flag
        isInitialized = true;  // Set the flag
        setTimeout(async function() {
          db = await initDb("askgbp", "sessions", 1, "sessionid", "sessionid");
        //   if (props.operation.includes("set")) {
            await createRecord(db, "sessions", {"sessionid": props.sessionid, "timestamp": new Date().toISOString()});
            notifyHost({
              value: props.sessionid,
              dataType: "json",
            })
        //   }
        //   if (props.operation.includes("get")) {
            let records = await getAllRecords(db, "sessions");
            notifyHost({
              value: sortJsonListByTimestampDesc(records),
              dataType: "json",
            })
        //   }
        }, 5);
      }
    }
    // Access values sent from Streamlit!
    function dataUpdate_Handler(props) {

    }
    // Simply log received data dictionary
    function log_Handler(props) {
      console.log("Received from Streamlit: " + JSON.stringify(props))
    }

    let pipeline = [initializeProps_Handler, dataUpdate_Handler, log_Handler]

    // ----------------------------------------------------
    // Finally, initialize component passing in pipeline

    initialize(pipeline)

  </script>
</body>

</html>

idxdbwrapper.js

async function initDb(db, store, version, key, idx, uniq=false) {
    return new Promise((resolve, reject) => {        
        let request = indexedDB.open(db, version);

        request.onerror = event => {
            alert('Error Event, check console');
            console.error(event);
        }
        
        request.onupgradeneeded = event => {
            console.log('idb onupgradeneeded firing');
            let db = event.target.result;
            let objectStore = db.createObjectStore(store, { keyPath: key, autoIncrement:true });
            objectStore.createIndex(idx, idx, { unique: uniq });
        };
        
        request.onsuccess = event => {
            resolve(event.target.result);
        };
    });
}

async function getAllRecords(db, s) {
    return new Promise((resolve, reject) => {
        let transaction = db.transaction([s], 'readonly');
        
        transaction.onerror = event => {
            reject(event);
        };
        
        let store = transaction.objectStore(s);
        store.getAll().onsuccess = event => {
            resolve(event.target.result);
        };    
    });
}

async function getAllRecordsByKey(db, s, k, idxname) {
    return new Promise((resolve, reject) => {
        let transaction = db.transaction([s], 'readonly');
        var objectStore = transaction.objectStore("logs");
        const index = objectStore.index(idxname);
        let query = index.getAll(k);

        query.onsuccess = function(event) { resolve(event.target.result); };
        query.onerror = function(event) { reject(event); };  
        // transaction.oncomplete  = function(event) { db.close(); };  
    });
}

async function createRecord(db, s, data) {
    return new Promise((resolve, reject) => {        
        let transaction = db.transaction([s], 'readwrite');
        transaction.oncomplete = event => {
            resolve();
        };
        
        transaction.onerror = event => {
            reject(event);
        };
        
        let store = transaction.objectStore(s);
        store.put(data);
    });
}

async function removeRecord(db, store, key) {
    return new Promise((resolve, reject) => {
        let transaction = db.transaction([store], 'readwrite');
        transaction.oncomplete = event => {
            resolve();
        };
        
        transaction.onerror = event => {
            reject(event);
        };
        
        let store = transaction.objectStore(store);
        store.delete(key);        
    });
}

function deleteDB(db) {
    let DBDeleteRequest = window.indexedDB.deleteDatabase(db);

    DBDeleteRequest.onerror = (event) => {
    console.error("Error deleting database.");
    };

    DBDeleteRequest.onsuccess = (event) => {
    console.log("Database deleted successfully");

    console.log(event.result); // should be undefined
    };
}

Does it work locally? If so, then make sure the component URL is correct in the Kubernetes environment. If not, then ensure it’s working locally then make any adjustments for Kubernetes.

Its working locally @asehmi . I am not sure what adjustments should I consider making in kubernetes though.
I am not using component url. here is my component’s init

import os
import streamlit.components.v1 as components

script_directory = os.path.dirname(os.path.abspath(__file__))
# target_directory = os.path.join(script_directory, 'idx_db_component', 'frontend')

idxdb_component = components.declare_component(
    name='idxdb_component',
    path=script_directory
)

[quote=“asehmi, post:31, topic:13064”]
component URL is correct in the Kubernetes environment
[/quote]What i noticed is. In the browser console it is printing error message

https:///component/idx_db_component.frontend.idxdb_component/index.html?streamlitUrl=https%3A%2F%2F%2F

Infact I am also facing same issue with extra_streamlit_components.CookieManager.cookie_manager