├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev.py ├── example.png ├── examples └── api_call.py ├── setup.py └── streamlit_javascript ├── __init__.py └── frontend ├── .env ├── .prettierrc ├── package.json ├── public └── index.html ├── src ├── JavascriptComponent.tsx └── index.tsx └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_javascript_dev", 3 | "image": "nikolaik/python-nodejs:latest", 4 | "workspaceFolder": "/code", 5 | "runArgs": [ 6 | "--network=host" 7 | ], 8 | "settings": { 9 | "terminal.integrated.shell.linux": "/bin/bash" 10 | }, 11 | "workspaceMount": "src=${localWorkspaceFolder},dst=/code,type=bind,consistency=cached", 12 | "postCreateCommand": "cd st_javascript/frontend && npm install && pip install streamlit", 13 | "extensions": [] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Verify package.json presence 30 | run: | 31 | ls -al streamlit_javascript/frontend 32 | 33 | - name: Build release distributions 34 | run: | 35 | # NOTE: put your own distribution build steps here. 36 | python -m pip install build 37 | python -m build 38 | 39 | - name: Upload distributions 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: release-dists 43 | path: dist/ 44 | 45 | pypi-publish: 46 | runs-on: ubuntu-latest 47 | needs: 48 | - release-build 49 | permissions: 50 | # IMPORTANT: this permission is mandatory for trusted publishing 51 | id-token: write 52 | 53 | # Dedicated environments with protections for publishing are strongly recommended. 54 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 55 | environment: 56 | name: pypi 57 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 58 | # url: https://pypi.org/p/streamlit-javascript 59 | # 60 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 61 | # ALTERNATIVE: exactly, uncomment the following line instead: 62 | url: https://pypi.org/project/streamlit-javascript/${{ github.event.release.name }} 63 | 64 | steps: 65 | - name: Retrieve release distributions 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: release-dists 69 | path: dist/ 70 | 71 | - name: Publish release distributions to PyPI 72 | uses: pypa/gh-action-pypi-publish@release/v1 73 | with: 74 | packages-dir: dist/ 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Project specific files not to be sent to git file history 3 | ######################################################################## 4 | streamlit_javascript/frontend/node_modules/ 5 | streamlit_javascript/frontend/.eslintcache 6 | streamlit_javascript.egg-info/* 7 | streamlit_javascript.egg-info/PKG-INFO 8 | 9 | 10 | ######################################################################## 11 | # Python - https://github.com/github/gitignore/blob/master/Python.gitignore 12 | ######################################################################## 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | *.dll 21 | 22 | # Distribution / packaging 23 | .Python 24 | .installed.cfg 25 | MANIFEST 26 | build/ 27 | dist/ 28 | downloads/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg 37 | eggs/ 38 | .eggs/ 39 | *.egg-info/ 40 | develop-eggs/ 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | Pipfile.lock 46 | Pipfile 47 | 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | cover/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | .pybuilder/ 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | # For a library or package, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # .python-version 105 | 106 | # pipenv 107 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 108 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 109 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 110 | # install all needed dependencies. 111 | #Pipfile.lock 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | ######################################################################## 157 | # OSX - https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 158 | ######################################################################## 159 | .DS_Store 160 | .DocumentRevisions-V100 161 | .fseventsd 162 | .Spotlight-V100 163 | .TemporaryItems 164 | .Trashes 165 | .VolumeIcon.icns 166 | .com.apple.timemachine.donotpresent 167 | 168 | ######################################################################## 169 | # node - https://github.com/github/gitignore/blob/master/Node.gitignore 170 | ######################################################################## 171 | # Dependency directories 172 | .yarn/ 173 | node_modules/ 174 | 175 | # pnp files 176 | .pnp.*js 177 | 178 | # Logs 179 | npm-debug.log* 180 | yarn-debug.log* 181 | yarn-error.log* 182 | 183 | # Coverage directory used by tools like istanbul 184 | coverage/ 185 | 186 | # JS/ES lint 187 | .eslintcache 188 | 189 | # Lockfiles 190 | yarn.lock 191 | package-lock.json 192 | poetry.lock 193 | 194 | ######################################################################## 195 | # JetBrains 196 | ######################################################################## 197 | .idea 198 | 199 | ######################################################################## 200 | # VSCode 201 | ######################################################################## 202 | .vscode/ 203 | 204 | ######################################################################## 205 | # vim 206 | ######################################################################## 207 | .vim/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alexander Balasch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include streamlit_*/frontend/build * 2 | include streamlit_*/frontend/package.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *Streamlit javascript execution extension* 2 | 3 | [![GitHub][github_badge]][github_link] [![PyPI][pypi_badge]][pypi_link] 4 | 5 | ## Installation using pypi 6 | Activate your python virtual environment 7 | ```sh 8 | pip install streamlit-javascript>=1.42.0 9 | ``` 10 | ## Installation using github source 11 | Activate your python virtual environment 12 | ```sh 13 | pip install git+https://github.com/thunderbug1/streamlit-javascript.git@1.42.0 14 | ``` 15 | ## Installation using local source 16 | Activate your python virtual environment 17 | ```sh 18 | git clone https://github.com/thunderbug1/streamlit-javascript.git 19 | cd streamlit-javascript 20 | pip install . 21 | ``` 22 | ## Installing tools required for build 23 | You may need to install some packages to build the source 24 | ```sh 25 | # APT 26 | sudo apt install python-pip protobuf-compiler libgconf-2-4 27 | # HOMEBREW 28 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 29 | brew install protobuf graphviz gawk 30 | # YARN v4 - if you set PACKAGE_MGR="yarn" in setup.py 31 | sudo npm uninstall --global yarn 32 | corepack enable || sudo npm install --global corepack && corepack enable 33 | ``` 34 | 35 | ## Running a local development environment (hot source update) 36 | Activate your python virtual environment 37 | ```sh 38 | git clone https://github.com/thunderbug1/streamlit-javascript.git 39 | cd streamlit-javascript 40 | pip install -e . 41 | 42 | # NPM option - if you set PACKAGE_MGR="npm" in setup.py 43 | (cd streamlit_javascript/frontend && npm install -D) 44 | (cd streamlit_javascript/frontend && npm run start) 45 | # YARN alternate - if you set PACKAGE_MGR="yarn" in setup.py 46 | (cd streamlit_javascript/frontend && yarn install --production=false) 47 | (cd streamlit_javascript/frontend && yarn start) 48 | ``` 49 | ### which will run this streamlit site concurrently with the following command 50 | ```sh 51 | streamlit run dev.py --browser.serverAddress localhost --browser.gatherUsageStats false 52 | ``` 53 | This allows hot reloading of both the streamlit python and ReAct typescript 54 | 55 | ## Debugging python in a local development environment (hot source update) 56 | Activate your python virtual environment 57 | ```sh 58 | git clone https://github.com/thunderbug1/streamlit-javascript.git 59 | cd streamlit-javascript 60 | pip install -e . 61 | 62 | # NPM option - if you set PACKAGE_MGR="npm" in setup.py 63 | (cd streamlit_javascript/frontend && npm run hottsx) 64 | # YARN alternate - if you set PACKAGE_MGR="yarn" in setup.py 65 | (cd streamlit_javascript/frontend && yarn hottsx) 66 | ``` 67 | ### Now run this in your debugging tool 68 | Remembering to match your python virtual environment in the debugger 69 | ```sh 70 | streamlit run dev.py --browser.serverAddress localhost --browser.gatherUsageStats false 71 | ``` 72 | This sill allows hot reloading of both the streamlit python and ReAct typescript 73 | 74 | ## Using st_javascript in your code 75 | You can look at dev.py for working examples by getting the github source 76 | ### Simple expression 77 | ```py 78 | import streamlit as st 79 | from streamlit_javascript import st_javascript 80 | 81 | st.subheader("Javascript API call") 82 | return_value = st_javascript("1+1") 83 | st.markdown(f"Return value was: {return_value}") 84 | ``` 85 | ### An in place function (notice the brace positions) 86 | ```py 87 | return_value = st_javascript("(function(){ return window.parent.document.body.clientWidth; })()") 88 | ``` 89 | ### An async place function (notice the brace positions) 90 | ```py 91 | return_value = st_javascript(""" 92 | (async function(){ 93 | return await fetch("https://reqres.in/api/products/3") 94 | .then(function(response) {return response.json();}); 95 | })() 96 | ""","Waiting for response") 97 | ``` 98 | ### A muplitple setComponentValue 99 | ```py 100 | st.markdown("Browser Time: "+st_javascript("today.toUTCString()","...","TODAY",1000)) 101 | ``` 102 | ### An on_change muplitple setComponentValue (with a block while we wait for the first return value) 103 | ```py 104 | def width_changed() -> None: 105 | st.toast(st.session_state['WIDTH']) 106 | return_value = st_javascript("window.parent.document.body.clientWidth",None,"WIDTH",1000,width_changed) 107 | if return_value is None: 108 | st.stop() 109 | ``` 110 | ### You can also this code at the top of your page to hide the code frames 111 | ```py 112 | st.markdown("""""", unsafe_allow_html=True) 113 | ``` 114 | 115 | [github_badge]: https://badgen.net/badge/icon/GitHub?icon=github&color=black&label 116 | [github_link]: https://github.com/thunderbug1/streamlit-javascript 117 | 118 | [pypi_badge]: https://badge.fury.io/py/streamlit-javascript.svg 119 | [pypi_link]: https://pypi.org/project/streamlit-javascript/ 120 | -------------------------------------------------------------------------------- /dev.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | import streamlit as st 3 | from streamlit.runtime.state.common import WidgetCallback 4 | 5 | # Replace st_javascript to use the URL version which does hot reload in combination with `pkg_mgr` run start 6 | from streamlit_javascript import st_javascript as st_javascript_noupdate 7 | 8 | 9 | def st_javascript_hotupdates( 10 | js_code: str, 11 | default: Any = 0, 12 | key: str | None = None, 13 | poll: int = 0, 14 | on_change: WidgetCallback | None = None, 15 | ) -> Any: 16 | return st_javascript_noupdate(js_code, default, key, poll, on_change, True) 17 | 18 | 19 | ################################################################################ 20 | st.set_page_config("Hot Reload Development", ":hotsprings:", layout="wide") 21 | 22 | 23 | ################################################################################ 24 | # This HTML hides the iframe components so they don't use space on the page 25 | st.markdown( 26 | """ 27 | 31 | """, 32 | unsafe_allow_html=True, 33 | ) 34 | 35 | 36 | ################################################################################ 37 | st.subheader("Executing browser javascript simple expression:") 38 | js_expr = "1+2+3" 39 | st.code(js_expr, "javascript") 40 | st.markdown(f"Return value was: {st_javascript_hotupdates(js_expr)}") 41 | print(f"Return value was:") 42 | 43 | 44 | ################################################################################ 45 | st.subheader("Executing browser javascript async code:") 46 | js_code = """ 47 | (async function(){ 48 | return await fetch("https://reqres.in/api/products/3") 49 | .then(function(response) {return response.json();}); 50 | })() 51 | """ 52 | st.code(js_code, "typescript", line_numbers=True) 53 | return_json = st_javascript_hotupdates(js_code, {"waiting for browser": "Async = TRUE"}) 54 | st.json(return_json) 55 | 56 | ################################################################################ 57 | st.subheader( 58 | "Executing browser javascript immediate function, also using on_change and poll=1000:" 59 | ) 60 | js_code = "(function(){ return window.parent.document.body.clientWidth; })()" 61 | st.markdown( 62 | "***This has to be use the path component (normal install) so it can escape from the iframe***" 63 | ) 64 | st.markdown( 65 | "***But only URL:3003 components are hot source updated, so changes to .ts files need a program restart***" 66 | ) 67 | st.code(js_code, "javascript") 68 | st.markdown( 69 | f"ScreenWidth (using port 3003)={st_javascript_hotupdates(js_code,"?","WIDTH_URL")}" 70 | ) 71 | 72 | 73 | def width_change() -> None: 74 | st.toast(f"width_change() callback={st.session_state['WIDTH_PATH']}") 75 | print(f"width_change() callback={st.session_state['WIDTH_PATH']}") 76 | 77 | 78 | st.markdown( 79 | f"ScreenWidth (using release path)={st_javascript_noupdate(js_code,"?","WIDTH_PATH", 1000, width_change)}" 80 | ) 81 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thunderbug1/streamlit-javascript/db0bdc9fdfd24f108f694c7692bb04309ef6c5fe/example.png -------------------------------------------------------------------------------- /examples/api_call.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from streamlit_javascript import st_javascript 3 | 4 | js_code = """await fetch("https://reqres.in/api/products/3") 5 | .then(function(response) {return response.json();})""" 6 | 7 | st.subheader("Executing javascript code:") 8 | st.markdown(f"""``` 9 | {js_code}""") 10 | 11 | return_value = st_javascript(js_code) 12 | st.markdown(f"Return value was: {return_value}") 13 | print(f"Return value was: {return_value}") 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from typing import Final, List 2 | import os 3 | import json 4 | import subprocess 5 | import setuptools 6 | from io import TextIOWrapper 7 | from subprocess import CompletedProcess 8 | from setuptools.command.build_py import build_py 9 | 10 | PACKAGE_MGR = "npm" 11 | PACKAGE_DIR = os.path.dirname(__file__) 12 | PACKAGE_NAME = "streamlit-javascript" 13 | STREAMLIT_VERSION = "1.42.0" # PEP-440 14 | 15 | 16 | class build_sdist(build_py): 17 | def run(self): 18 | build_py.run(self) 19 | self.run_command("Build-Frontend") 20 | 21 | 22 | class BuildErrorException(Exception): 23 | msg: Final[str] 24 | 25 | def __init__(self, msg): 26 | self.msg = msg 27 | 28 | 29 | class BuildFrontend(setuptools.Command): 30 | description: str = "Build ReAct interface" 31 | user_options: List = [] 32 | frontend_dir: str 33 | modules_dir: str 34 | build_dir: str 35 | log_file: TextIOWrapper 36 | 37 | def initialize_options(self) -> None: 38 | pass 39 | 40 | def finalize_options(self) -> None: 41 | self.frontend_dir = os.path.join( 42 | PACKAGE_DIR, "streamlit_javascript", "frontend" 43 | ) 44 | self.modules_dir = os.path.join(self.frontend_dir, "node_modules") 45 | self.build_dir = os.path.join(self.frontend_dir, "build") 46 | 47 | def msg_log(self, msg: str, /, indent: int = 0) -> None: 48 | assert self.log_file.writable() 49 | for line in msg.splitlines(): 50 | if len(line.strip()) > 0: 51 | self.log_file.write(" " * indent + line + os.linesep) 52 | self.log_file.flush() 53 | 54 | def msg_run(self, result, /, indent: int): 55 | self.msg_log(f"RC:{result.returncode}", indent=indent) 56 | self.msg_log("STDOUT:", indent=indent) 57 | self.msg_log(result.stdout, indent=indent + 2) 58 | self.msg_log("STDERR:", indent=indent) 59 | self.msg_log(result.stderr, indent=indent + 2) 60 | return result 61 | 62 | def check_package_json(self): 63 | self.msg_log("Checking package.json version...") 64 | with open( 65 | os.path.join(self.frontend_dir, "package.json"), 66 | mode="r", 67 | encoding="utf-8", 68 | ) as pkg_json: 69 | try: 70 | pkg_desc = json.load(pkg_json) 71 | if "version" not in pkg_desc: 72 | self.msg_log( 73 | f"WARNING\xe2\x9a\xa0: package.json:version is missing, should be {STREAMLIT_VERSION}" 74 | ) 75 | elif pkg_desc["version"] != STREAMLIT_VERSION: 76 | self.msg_log( 77 | f"WARNING\xe2\x9a\xa0: package.json:version should be {STREAMLIT_VERSION} not {pkg_desc["version"]}" 78 | ) 79 | except json.decoder.JSONDecodeError as exc: 80 | self.msg_log("Unable to read package.JSON file - syntax error") 81 | raise json.decoder.JSONDecodeError( 82 | "package.json: " + exc.msg, 83 | os.path.join(self.frontend_dir, "package.json"), 84 | exc.pos, 85 | ) from None 86 | 87 | def check_need_protobuff(self): 88 | # TODO: streamlit-javascript does not use protobuf, but we should have a test 89 | self.msg_log(f"{PACKAGE_NAME} does not use protobuf...") 90 | 91 | def show_msg_if_build_dir_exists(self): 92 | self.msg_log("Checking if Fontend has already been built...") 93 | if os.path.isdir(self.build_dir): 94 | self.msg_log("Found build directory", indent=2) 95 | 96 | def show_msg_if_modules_dir_exists(self): 97 | self.msg_log("Checking if node_modules exists...") 98 | if os.path.isdir(self.modules_dir): 99 | self.msg_log("Found build directory", indent=2) 100 | 101 | def check_node_installed(self) -> CompletedProcess: 102 | self.msg_log("Checking node is installed...") 103 | result: CompletedProcess = self.msg_run( 104 | subprocess.run( 105 | ["node", "--version"], 106 | executable="node", 107 | cwd=PACKAGE_DIR, 108 | capture_output=True, 109 | text=True, 110 | encoding="utf-8", 111 | ), 112 | indent=2, 113 | ) 114 | if result.returncode != 0: 115 | raise BuildErrorException( 116 | "Could not find node - it is required for ReAct components" 117 | ) 118 | return result 119 | 120 | def check_pkgmgr_installed(self) -> CompletedProcess: 121 | self.msg_log(f"Checking {PACKAGE_MGR} is installed...") 122 | result = self.msg_run( 123 | subprocess.run( 124 | [PACKAGE_MGR, "--version"], 125 | executable=PACKAGE_MGR, 126 | cwd=PACKAGE_DIR, 127 | capture_output=True, 128 | text=True, 129 | encoding="utf-8", 130 | ), 131 | indent=2, 132 | ) 133 | if PACKAGE_MGR == "yarn": 134 | self.msg_log("Checking yarn corepack is installed") 135 | result = self.msg_run( 136 | subprocess.run( 137 | ["corepack", "enable"], 138 | executable="corepack", 139 | cwd=PACKAGE_DIR, 140 | capture_output=True, 141 | text=True, 142 | encoding="utf-8", 143 | ), 144 | indent=2, 145 | ) 146 | if result.returncode != 0: 147 | raise BuildErrorException( 148 | f"Could not find corepack/{PACKAGE_MGR} - it is required to install node packages" 149 | ) 150 | if result.returncode != 0: 151 | raise BuildErrorException( 152 | f"Could not find {PACKAGE_MGR} - it is required to install node packages" 153 | ) 154 | return result 155 | 156 | def run_install(self) -> CompletedProcess: 157 | self.msg_log(f"Running {PACKAGE_MGR} install...") 158 | result = self.msg_run( 159 | subprocess.run( 160 | [PACKAGE_MGR, "install"], 161 | executable=PACKAGE_MGR, 162 | cwd=self.frontend_dir, 163 | capture_output=True, 164 | text=True, 165 | encoding="utf-8", 166 | ), 167 | indent=2, 168 | ) 169 | return result 170 | 171 | def run_build(self) -> CompletedProcess: 172 | self.msg_log(f"Running {PACKAGE_MGR} run build...") 173 | result = self.msg_run( 174 | subprocess.run( 175 | [PACKAGE_MGR, "run", "build"], 176 | executable=PACKAGE_MGR, 177 | cwd=self.frontend_dir, 178 | capture_output=True, 179 | text=True, 180 | encoding="utf-8", 181 | ), 182 | indent=2, 183 | ) 184 | return result 185 | 186 | def run_npm_audit(self) -> CompletedProcess: 187 | self.msg_log("Running npm audit...") 188 | result = self.msg_run( 189 | subprocess.run( 190 | [PACKAGE_MGR, "audit"], 191 | executable=PACKAGE_MGR, 192 | cwd=self.frontend_dir, 193 | capture_output=True, 194 | text=True, 195 | encoding="utf-8", 196 | ), 197 | indent=2, 198 | ) 199 | return result 200 | 201 | def check_build_output_ok(self): 202 | self.msg_log("Checking if Fontend was built...") 203 | if os.path.isdir(self.build_dir): 204 | self.msg_log("Found build directory", indent=2) 205 | else: 206 | raise BuildErrorException("Failed to create output directory") 207 | 208 | def run(self) -> None: 209 | original_directory = os.getcwd() 210 | try: 211 | os.chdir(PACKAGE_DIR) 212 | self.log_file = open( 213 | "setup.log", 214 | mode="w", 215 | encoding="utf-8", 216 | ) 217 | # PreInstallation Checks 218 | self.check_package_json() 219 | self.check_need_protobuff() 220 | self.show_msg_if_build_dir_exists() 221 | self.show_msg_if_modules_dir_exists() 222 | self.check_node_installed() 223 | self.check_pkgmgr_installed() 224 | # RunInstallation 225 | result = self.run_install() 226 | if result.stdout.find("npm audit fix") != -1: 227 | result = self.run_npm_audit() 228 | result = self.run_build() 229 | # PostInstallation Checks 230 | self.check_build_output_ok() 231 | finally: 232 | if not self.log_file.closed: 233 | self.log_file.close() 234 | os.chdir(original_directory) 235 | 236 | 237 | readme_path = os.path.join(PACKAGE_DIR, "README.md") 238 | long_description = "" 239 | if os.path.exists(readme_path): 240 | with open(readme_path, "r", encoding="utf-8") as fh: 241 | long_description = fh.read() 242 | 243 | setuptools.setup( 244 | name=PACKAGE_NAME, 245 | version=STREAMLIT_VERSION, 246 | description="component to run javascript code in streamlit application", 247 | long_description=long_description, 248 | long_description_content_type="text/markdown", 249 | url="https://github.com/thunderbug1/streamlit-javascript", 250 | author="Alexander Balasch & Strings", 251 | author_email="", 252 | license="MIT License", 253 | classifiers=[ 254 | "Intended Audience :: Developers", 255 | "Intended Audience :: Science/Research", 256 | "Programming Language :: Python :: 3.9", 257 | "Programming Language :: Python :: 3.10", 258 | "Programming Language :: Python :: 3.11", 259 | "Programming Language :: Python :: 3.12", 260 | "Programming Language :: Python :: 3.13", 261 | "Operating System :: OS Independent", 262 | ], 263 | install_requires=[ 264 | "streamlit >= " + STREAMLIT_VERSION, 265 | ], 266 | python_requires=">=3.9, !=3.9.7", # match streamlit v1.42.0 267 | # PEP 561: https://mypy.readthedocs.io/en/stable/installed_packages.html 268 | packages=setuptools.find_packages(), 269 | cmdclass={ 270 | "build_py": build_sdist, 271 | "Build-Frontend": BuildFrontend, 272 | }, 273 | zip_safe=False, # install source files not egg 274 | include_package_data=True, # copy html and friends 275 | ) 276 | -------------------------------------------------------------------------------- /streamlit_javascript/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | import streamlit.components.v1 as components 4 | from streamlit.runtime.state.common import WidgetCallback 5 | 6 | 7 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 8 | build_dir = os.path.join(parent_dir, "frontend", "build") 9 | _component_func_path = components.declare_component( 10 | "streamlit_javascript", path=build_dir 11 | ) 12 | 13 | _component_func_url = components.declare_component( 14 | "streamlit_javascript_url", 15 | # Pass `url` here to tell Streamlit that the component will be served 16 | # by the local dev server that you run via `npm run start`. 17 | url="http://localhost:3003", 18 | ) 19 | 20 | 21 | def st_javascript( 22 | js_code: str, 23 | default: Any = 0, 24 | key: str | None = None, 25 | poll: int = 0, 26 | on_change: WidgetCallback | None = None, 27 | _use_url=False, 28 | ) -> Any: 29 | """Create a new instance of "st_javascript". 30 | 31 | Parameters 32 | ---------- 33 | js_code: str 34 | The javascript expression that is to be evaluated on the client side. 35 | It can be synchronous or asynchronous. 36 | default: any or None 37 | The default return value for the component. This is returned when 38 | the component's frontend hasn't yet specified a value with 39 | `setComponentValue`. 40 | key: str or None 41 | An optional key that uniquely identifies this component. If this is 42 | None, and the component's arguments are changed, the component will 43 | be re-mounted in the Streamlit frontend and lose its current state. 44 | poll: int 45 | If greater than 0, the number of milliseconds to pause between repeatedly 46 | checking the value of the javascript expression, and calling 47 | `setComponentValue` for each change 48 | on_change: callback function with no arguments returning None 49 | Will be called each time the expression evaluation changes, best used 50 | in combination with poll, and key so you can access the updated value with 51 | st.session_state[key] 52 | 53 | 54 | Returns 55 | ------- 56 | obj 57 | The result of the executed javascript expression 58 | """ 59 | # Call through to our private component function. Arguments we pass here 60 | # will be sent to the frontend, where they'll be available in an "args" 61 | # dictionary. 62 | if _use_url: 63 | _component_func = _component_func_url 64 | else: 65 | _component_func = _component_func_path 66 | component_value = _component_func( 67 | js_code=js_code, 68 | default=default, 69 | key=key, 70 | poll=poll, 71 | on_change=on_change, 72 | height=0, 73 | width=0, 74 | ) 75 | # We could modify the value returned from the component if we wanted. 76 | return component_value 77 | -------------------------------------------------------------------------------- /streamlit_javascript/frontend/.env: -------------------------------------------------------------------------------- 1 | # Run the component's dev server on :3003 2 | # (The Streamlit dev server already runs on :3000) 3 | PORT=3003 4 | 5 | # Don't automatically open the web browser on `npm run start`. 6 | BROWSER=none 7 | -------------------------------------------------------------------------------- /streamlit_javascript/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /streamlit_javascript/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_javascript", 3 | "version": "1.42.0", 4 | "private": true, 5 | "packageManager": "yarn@4.5.3", 6 | "dependencies": { 7 | "@types/node": "^18.11.17", 8 | "@types/react": "^18.2.0", 9 | "@types/react-dom": "^18.2.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "^5.0.1", 13 | "streamlit-component-lib": "^2.0.0", 14 | "typescript": "^4.0.0" 15 | }, 16 | "devDependencies": { 17 | "concurrently": "^9.1.2" 18 | }, 19 | "//": "Overrides to fix vulnerabilities", 20 | "overrides": { 21 | "nth-check": "^2.0.1", 22 | "postcss": "^8.4.31" 23 | }, 24 | "scripts": { 25 | "start": "concurrently \"BROWSER=none react-scripts start\" \"streamlit run ../../dev.py --browser.serverAddress localhost --browser.gatherUsageStats false\"", 26 | "hottsx": "BROWSER=none react-scripts start", 27 | "build": "react-scripts build" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "homepage": "." 42 | } -------------------------------------------------------------------------------- /streamlit_javascript/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit JavaScript Component 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /streamlit_javascript/frontend/src/JavascriptComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | StreamlitComponentBase, 4 | withStreamlitConnection, 5 | } from "streamlit-component-lib" 6 | import { ReactNode } from "react" 7 | 8 | interface State { 9 | result: unknown, 10 | has_run: boolean 11 | } 12 | 13 | class JavascriptComponent extends StreamlitComponentBase { 14 | constructor(props: any) { 15 | super(props); 16 | this.state = { result: "", has_run: false } 17 | } 18 | async componentDidMount() { 19 | const js_code = this.props.args["js_code"] 20 | const poll_rate = this.props.args["poll"] 21 | 22 | if (!this.state.has_run) { 23 | let prev_result = this.props.args["default"] 24 | do { 25 | let result = "" 26 | try { 27 | // eslint-disable-next-line 28 | result = await eval(js_code) 29 | } catch (e) { 30 | result = String(e) 31 | } 32 | 33 | if (result != prev_result) { 34 | prev_result = result 35 | this.setState( 36 | prevState => ({ result: result, has_run: true }), 37 | () => Streamlit.setComponentValue(this.state.result) 38 | ) 39 | } 40 | if (poll_rate) 41 | await new Promise(r => setTimeout(r, poll_rate)); 42 | } while (poll_rate) 43 | } 44 | } 45 | 46 | public render = (): ReactNode => { 47 | return null 48 | } 49 | 50 | } 51 | 52 | export default withStreamlitConnection(JavascriptComponent) -------------------------------------------------------------------------------- /streamlit_javascript/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from 'react-dom/client' 3 | import MyComponent from "./JavascriptComponent" 4 | 5 | const container = document.getElementById('root'); 6 | const root = createRoot(container!); 7 | root.render( 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /streamlit_javascript/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------