├── .flake8 ├── .github └── workflows │ └── publish_PYPI_each_tag.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src └── st_keyup │ ├── __init__.py │ └── frontend │ ├── index.html │ ├── main.js │ ├── streamlit-component-lib.js │ └── style.css └── streamlit_app.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | select = 4 | E # pep8 errors 5 | F # pyflakes errors 6 | W # pep8 warnings 7 | B # flake8-bugbear warnings 8 | ignore = 9 | E501 # "Line lengths are recommended to be no greater than 79 characters" 10 | E203 # "Whitespace before ':'": conflicts with black 11 | W503 # "line break before binary operator": conflicts with black 12 | exclude = 13 | .git 14 | .vscode 15 | .pytest_cache 16 | .mypy_cache 17 | .venv 18 | .env 19 | .direnv 20 | per-file-ignores = -------------------------------------------------------------------------------- /.github/workflows/publish_PYPI_each_tag.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.x" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install setuptools wheel twine 24 | - name: Build and publish 25 | env: 26 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run: | 29 | pwd 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Python - https://github.com/github/gitignore/blob/master/Python.gitignore 3 | ######################################################################## 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packaging 10 | build/ 11 | dist/ 12 | eggs/ 13 | .eggs/ 14 | *.egg-info/ 15 | *.egg 16 | 17 | # Unit test / coverage reports 18 | .coverage 19 | .coverage\.* 20 | .pytest_cache/ 21 | .mypy_cache/ 22 | test-reports 23 | 24 | # Test fixtures 25 | cffi_bin 26 | 27 | # Pyenv Stuff 28 | .python-version 29 | 30 | # testing with seleniumbase 31 | latest_logs 32 | archived_logs 33 | current-screenshot.png 34 | 35 | ######################################################################## 36 | # OSX - https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 37 | ######################################################################## 38 | .DS_Store 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | ######################################################################## 48 | # node - https://github.com/github/gitignore/blob/master/Node.gitignore 49 | ######################################################################## 50 | # Logs 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # Dependency directories 56 | node_modules/ 57 | 58 | # Coverage directory used by tools like istanbul 59 | coverage/ 60 | 61 | # Lockfiles 62 | yarn.lock 63 | package-lock.json 64 | 65 | ######################################################################## 66 | # JetBrains 67 | ######################################################################## 68 | .idea 69 | 70 | ######################################################################## 71 | # VSCode 72 | ######################################################################## 73 | .vscode/ 74 | 75 | .direnv/ 76 | .envrc 77 | 78 | .streamlit/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.8.3 4 | hooks: 5 | - id: ruff 6 | args: 7 | - --fix 8 | - repo: https://github.com/pre-commit/mirrors-mypy 9 | rev: "v1.13.0" 10 | hooks: 11 | - id: mypy 12 | args: 13 | - --ignore-missing-imports 14 | - --follow-imports=silent 15 | 16 | - repo: https://github.com/pre-commit/pre-commit-hooks 17 | rev: v4.6.0 # Use the ref you want to point at 18 | hooks: 19 | - id: trailing-whitespace 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Zachary Blackwood 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/st_keyup/frontend * 2 | include README.md 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-keyup 2 | 3 | [](https://pypi.org/project/streamlit-keyup/) 4 | [](https://pypistats.org/packages/streamlit-keyup) 5 | [](LICENSE) 6 | [](https://github.com/psf/black) 7 | 8 | If you're collecting text input from your users in your streamlit app, `st.text_input` works well -- as long as you're happy with 9 | waiting to get the response when they're finished typing. 10 | 11 | But, what if you want to get the input out, and do something with it every time they type a new key (AKA "on keyup")? 12 | 13 | [](https://key-up.streamlitapp.com) 14 | 15 |  16 | 17 | ## Installation 18 | 19 | `pip install streamlit-keyup` 20 | 21 | ## Usage 22 | 23 | ```python 24 | import streamlit as st 25 | from st_keyup import st_keyup 26 | 27 | value = st_keyup("Enter a value", key="0") 28 | 29 | # Notice that value updates after every key press 30 | st.write(value) 31 | 32 | # If you want to set a default value, you can pass one 33 | with_default = st_keyup("Enter a value", value="Example", key="1") 34 | 35 | # If you want to limit how often the value gets updated, pass `debounce` value, which 36 | # will force the value to only update after that many milliseconds have passed 37 | with_debounce = st_keyup("Enter a value", debounce=500, key="2") 38 | ``` 39 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 88 3 | select = ["E", "F", "I001"] 4 | 5 | [tool.mypy] 6 | files = ["**/*.py"] 7 | follow_imports = "silent" 8 | ignore_missing_imports = true 9 | scripts_are_modules = true 10 | python_version = 3.9 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit>=1.2.0 2 | streamlit-keyup>=0.1.9 3 | jinja2 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import setuptools 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setuptools.setup( 9 | name="streamlit-keyup", 10 | version="0.3.0", 11 | author="Zachary Blackwood", 12 | author_email="zachary@streamlit.io", 13 | description="Text input that renders on keyup", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/blackary/streamlit-keyup", 17 | packages=setuptools.find_packages(where="src"), 18 | package_dir={"": "src"}, 19 | include_package_data=True, 20 | classifiers=[], 21 | python_requires=">=3.7", 22 | install_requires=["streamlit>=1.2", "jinja2"], 23 | ) 24 | -------------------------------------------------------------------------------- /src/st_keyup/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any, Callable, Dict, Optional, Tuple 3 | 4 | import streamlit as st 5 | import streamlit.components.v1 as components 6 | 7 | build_dir = Path(__file__).parent.absolute() / "frontend" 8 | _component_func = components.declare_component("st_keyup", path=str(build_dir)) 9 | 10 | 11 | def st_keyup( 12 | label: str, 13 | value: str = "", 14 | max_chars: Optional[int] = None, 15 | key: Optional[str] = None, 16 | type: str = "default", 17 | debounce: Optional[int] = None, 18 | on_change: Optional[Callable] = None, 19 | args: Optional[Tuple[Any, ...]] = None, 20 | kwargs: Optional[Dict[str, Any]] = None, 21 | *, 22 | placeholder: str = "", 23 | disabled: bool = False, 24 | label_visibility: str = "visible", 25 | ): 26 | """ 27 | Generate a text input that renders on keyup, debouncing the input by the 28 | specified amount of milliseconds. 29 | 30 | Debounce means that it will wait at least the specified amount of milliseconds 31 | before updating the value. This is useful for preventing excessive updates 32 | when the user is typing. Since the input updating will cause the app to rerun, 33 | if you are having performance issues, you should consider setting a debounce 34 | value. 35 | 36 | on_change is a callback function that will be called when the value changes. 37 | 38 | args and kwargs are optional arguments which are passed to the on_change callback 39 | function 40 | """ 41 | 42 | key_parts = [ 43 | key, 44 | disabled, 45 | label_visibility, 46 | debounce, 47 | max_chars, 48 | type, 49 | placeholder, 50 | ] 51 | 52 | computed_key = "st_keyup_" + "__".join( 53 | str(part) for part in key_parts if part is not None 54 | ) 55 | 56 | component_value = _component_func( 57 | label=label, 58 | value=value, 59 | key=computed_key, 60 | debounce=debounce, 61 | default=value, 62 | max_chars=max_chars, 63 | type=type, 64 | placeholder=placeholder, 65 | disabled=disabled, 66 | label_visibility=label_visibility, 67 | ) 68 | 69 | if key is not None: 70 | st.session_state[key] = component_value 71 | 72 | if key is None: 73 | key = "st_keyup_" + label 74 | 75 | if on_change is not None: 76 | if "__previous_values__" not in st.session_state: 77 | st.session_state["__previous_values__"] = {} 78 | 79 | if component_value != st.session_state["__previous_values__"].get(key, value): 80 | st.session_state["__previous_values__"][key] = component_value 81 | 82 | if args is None: 83 | args = () 84 | if kwargs is None: 85 | kwargs = {} 86 | on_change(*args, **kwargs) 87 | 88 | return component_value 89 | 90 | 91 | def main(): 92 | from datetime import datetime 93 | 94 | st.write("## Default keyup input") 95 | value = st_keyup("Enter a value") 96 | 97 | st.write(value) 98 | 99 | "## Keyup input with hidden label" 100 | value = st_keyup("You can't see this", label_visibility="hidden") 101 | 102 | "## Keyup input with collapsed label" 103 | value = st_keyup("This either", label_visibility="collapsed") 104 | 105 | "## Keyup with max_chars 5" 106 | value = st_keyup("Keyup with max chars", max_chars=5) 107 | 108 | "## Keyup input with password type" 109 | value = st_keyup("Password", value="Hello World", type="password") 110 | 111 | "## Keyup input with disabled" 112 | value = st_keyup("Disabled", value="Hello World", disabled=True) 113 | 114 | "## Keyup input with default value" 115 | value = st_keyup("Default value", value="Hello World") 116 | 117 | "## Keyup input with placeholder" 118 | value = st_keyup("Has placeholder", placeholder="A placeholder") 119 | 120 | "## Keyup input with 500 millesecond debounce" 121 | value = st_keyup("Enter a second value debounced", debounce=500) 122 | 123 | st.write(value) 124 | 125 | def on_change(): 126 | st.write("Value changed!", datetime.now()) 127 | 128 | def on_change2(*args, **kwargs): 129 | st.write("Value changed!", args, kwargs) 130 | 131 | "## Keyup input with on_change callback" 132 | value = st_keyup("Has an on_change", on_change=on_change) 133 | 134 | "## Keyup input with on_change callback and debounce" 135 | value = st_keyup("On_change + debounce", on_change=on_change, debounce=1000) 136 | st.write(value) 137 | 138 | "## Keyup input with args" 139 | value = st_keyup( 140 | "Enter a fourth value...", 141 | on_change=on_change2, 142 | args=("Hello", "World"), 143 | kwargs={"foo": "bar"}, 144 | ) 145 | st.write(value) 146 | 147 | "## Standard text input for comparison" 148 | value = st.text_input("Enter a value") 149 | 150 | st.write(value) 151 | 152 | st.write(st.session_state) 153 | 154 | 155 | if __name__ == "__main__": 156 | main() 157 | -------------------------------------------------------------------------------- /src/st_keyup/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |