├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── app.py ├── frontend ├── __init__.py ├── __pycache__ │ └── __init__.cpython-38.pyc └── index.html ├── frontend_multipage_app ├── __init__.py └── index.html ├── images ├── streamlit-component-toggle-buttons.gif └── streamlit-component-toggle-buttons.png ├── multipage_app.py └── run.cmd /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "env": {"DEBUG": "true"} 14 | }, 15 | { 16 | "name": "Python: Remote Attach", 17 | "type": "python", 18 | "request": "attach", 19 | "port": 6789, 20 | "host": "localhost", 21 | "justMyCode": false, 22 | "redirectOutput": true, 23 | "pathMappings": [ 24 | { 25 | "localRoot": "${workspaceFolder}", 26 | "remoteRoot": "." 27 | } 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arvindra 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamlit toggle buttons component 2 | 3 | > Arvindra Sehmi, CloudOpti Ltd. | [Website](https://sehmiconscious.blogspot.com) | [LinkedIn](https://www.linkedin.com/in/asehmi/) 4 | 5 | > Updated: 28 September, 2021 6 | 7 | --- 8 | 9 | **TL;DR:** Implementation of toggle buttons as a pure static HTML/JS Streamlit component. The key learning from this 10 | sample application is that the Streamlit app server and front end web server are not required to be separate. _The component implementation is in a plain HTML/JS file, which is loaded, executed and renderd directly from the Streamlit app server_. 11 | 12 | ## Demo 13 | 14 | ![streamlit-component-toggle-buttons](./images/streamlit-component-toggle-buttons.gif) 15 | 16 | ## Demo Multipage App 17 | 18 | You can find a complete mini multipage app implementation in `./frontend_multipage_app`, which uses this simple framework. Run the app from the root folder: 19 | 20 | ``` 21 | $ streamlit run multipage_app.py 22 | You can now view your Streamlit app in your browser. 23 | 24 | Local URL: http://localhost:8765 25 | Network URL: http://192.168.1.100:8765 26 | ``` 27 | 28 | ## Overview 29 | 30 | The component's front end implementation is in the `./frontend/` folder. There are two files here, `__init__.py` and `index.html`. 31 | 32 | **`./frontend/__init__.py`** 33 | 34 | This code (the presence of `__init__.py` makes it a Python module, actually) simply declares the component using Streamlit's `components.declare_component` API and exports a handle to it. This handle is `component_toggle_buttons`. You can see the `path` to the component is the same folder. When Streamlit loads the component it will serve the default `index.html` file from this location. 35 | 36 | ```python 37 | import streamlit.components.v1 as components 38 | component_toggle_buttons = components.declare_component( 39 | name='component_toggle_buttons', 40 | path='./frontend' 41 | ) 42 | ``` 43 | 44 | **`./frontend/index.html`** 45 | 46 | This self-contained file does the following: 47 | 48 | 1. Draws a simple HTML user interface with styled buttons, in two class groups 49 | 2. Loads JavaScript which implements core Streamlit component life cycle actions, namely the ability to: 50 | - Inform Streamlit client that the component is ready, using `streamlit:componentReady` message type. 51 | - Calculate or get it's own visible screen height, and inform Streamlit client, using `streamlit:setFrameHeight` message type. 52 | - Handle inbound `message` events from the Streamlit client; with `streamlit:render` event type being critical. 53 | - Send values (i.e., objects) to the Streamlit client application, using `streamlit:setComponentValue` message type. 54 | 55 | ```html 56 | 57 | 58 | 61 | 62 | 63 | 67 | 68 | 69 |

Awaiting value from Streamlit

70 | 71 | 72 | 73 | 74 |

Awaiting value from Streamlit

75 | 76 | 77 | 78 | 79 |
80 |
Awaiting value from Streamlit 81 |
82 | 83 | 84 | 226 | 227 | 228 | ``` 229 | 230 | In this basic component, notice `_sendMessage()` function uses `window.parent.postMessage()`, which is as fundamental as it gets. The value objects you send to the Streamlit client application must be any JSON serializable object. Conceptually they can be viewed as data or events carrying a data payload. Inbound message values received on `streamlit:render` events, are automatically de-serialized to JavaScript objects. 231 | 232 | It's only illustrative, but I have also implemented a simple pipeline of inbound message handlers and a dispatcher. I show this being used to initialize component data values, update the user interface, and to log output to the console. See `*_Handler()` functions, `pipeline`, `initialize()` function. 233 | 234 | The counterpart to the front end is the Streamlit application. Its entry point is in `app.py`. The `frontend` module is imported and the component handle, `component_toggle_buttons`, is used to create an instance of it. Interactions in the front end which give rise to value notifications will be received in the Streamlit client, which can be acted upon as required. I've provided simple design abstractions to make running of the component and handling its return values more explicit. They are `run_component()` and `handle_event()` respectively. This _wrapping_ makes the implementation neater and it'll be conceptually easier to understand the implementation of more advanced components when you're ready to take the next step implementing React component front ends. 235 | 236 | The HTML code for the toggle buttons groups a set of related buttons using a named class (my own convention). This group class name and the button instance is passed to the `onclick` handler, `toggle()`, which manages the styling for the `on/off` state, and 237 | creates a JSON object to hold the button group's current state and the state of the clicked button. The button `id`s in each group are numbered from `0` upwards (again by my own convention). Finally, the JSON object is communicated to the Streamlit 238 | client application using `notifyHost()`. 239 | 240 | Here's a brief explantion of the user interface elements (pink is `on` and dark grey is `off`): 241 | 242 | ![streamlit-component-toggle-buttons-image](./images/streamlit-component-toggle-buttons.png) 243 | 244 | And below is an example of the JSON object sent to the Streamlit client, indicating that button `Action 2.3` in button group `btn_group_2` was clicked to the `on` state. In addition, the state of all buttons in the group `btn_group_2` is provided. Feel free to change the JSON payload schema to suit your own requirements. 245 | 246 | ```json 247 | { 248 | "choice": { 249 | "name": "btn_group_2", 250 | "state": { 251 | "action": "Action 2.3", 252 | "value": true 253 | } 254 | }, 255 | "options": { 256 | "name": "btn_group_2", 257 | "states": [ 258 | { 259 | "action": "Action 2.1", 260 | "value": false 261 | }, 262 | { 263 | "action": "Action 2.2", 264 | "value": false 265 | }, 266 | { 267 | "action": "Action 2.3", 268 | "value": true 269 | } 270 | ] 271 | } 272 | } 273 | ``` 274 | 275 | ## Running the Toggle Buttons Component 276 | 277 | The component is run in the same way as any Streamlit app. 278 | 279 | - Open a console window and change directory to the root folder of the cloned GitHub repo, where `app.py` is. 280 | - Now run the Streamlit server with this app. 281 | 282 | ``` 283 | $ streamlit run app.py 284 | You can now view your Streamlit app in your browser. 285 | 286 | Local URL: http://localhost:8765 287 | Network URL: http://192.168.1.100:8765 288 | ``` 289 | 290 | - The app should start on the default port (8765) and launch a browser window to display the following page: 291 | 292 | --- 293 | 294 | All code is published under [MIT license](./LICENSE), so feel free to make changes and please **fork the repo if you're making changes and submit pull requests**. 295 | 296 | If you like this work, consider clicking that **star** button. Thanks! -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from datetime import datetime 3 | 4 | from frontend import component_toggle_buttons 5 | 6 | if 'counter' not in st.session_state: 7 | st.session_state.counter = 0 8 | 9 | def main(): 10 | def run_component(props): 11 | value = component_toggle_buttons(key='toggle_buttons', **props) 12 | return value 13 | def handle_event(value): 14 | st.header('Streamlit') 15 | st.write('Received from component: ', value) 16 | 17 | st.title('Toggle Buttons Component Demo') 18 | st.session_state.counter = st.session_state.counter + 1 19 | props = { 20 | 'initial_state': { 21 | 'group_1_header': 'Choose an option from group 1', 22 | 'group_2_header': 'Choose an option from group 2' 23 | }, 24 | 'counter': st.session_state.counter, 25 | 'datetime': str(datetime.now().strftime("%H:%M:%S, %d %b %Y")) 26 | } 27 | 28 | handle_event(run_component(props)) 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /frontend/__init__.py: -------------------------------------------------------------------------------- 1 | import streamlit.components.v1 as components 2 | component_toggle_buttons = components.declare_component( 3 | name='component_toggle_buttons', 4 | path='./frontend' 5 | ) -------------------------------------------------------------------------------- /frontend/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asehmi/streamlit-toggle-buttons-component/4ac842ca5ea0b40b2d1673c3d0313ea603f4a0cc/frontend/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | 52 | 53 | 57 | 58 | 59 |

Awaiting value from Streamlit

60 | 61 | 62 | 63 | 64 |

Awaiting value from Streamlit

65 | 66 | 67 | 68 | 69 |
70 |
Awaiting value from Streamlit 71 |
72 | 73 | 74 | 216 | 217 | -------------------------------------------------------------------------------- /frontend_multipage_app/__init__.py: -------------------------------------------------------------------------------- 1 | import streamlit.components.v1 as components 2 | component_multipage_app = components.declare_component( 3 | name='component_multipage_app', 4 | path='./frontend_multipage_app' 5 | ) -------------------------------------------------------------------------------- /frontend_multipage_app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 58 | 59 | 60 | 64 | 65 | 66 |
67 |
68 |   69 |
70 |
71 |

Awaiting value from Streamlit

72 |
73 |
74 |   75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 |
88 | 89 | 90 | 225 | 226 | -------------------------------------------------------------------------------- /images/streamlit-component-toggle-buttons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asehmi/streamlit-toggle-buttons-component/4ac842ca5ea0b40b2d1673c3d0313ea603f4a0cc/images/streamlit-component-toggle-buttons.gif -------------------------------------------------------------------------------- /images/streamlit-component-toggle-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asehmi/streamlit-toggle-buttons-component/4ac842ca5ea0b40b2d1673c3d0313ea603f4a0cc/images/streamlit-component-toggle-buttons.png -------------------------------------------------------------------------------- /multipage_app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | from frontend_multipage_app import component_multipage_app 4 | 5 | class MultiPage: 6 | def __init__(self): 7 | self.apps = [] 8 | self.app_names = [] 9 | 10 | def add_app(self, title, func, *args, **kwargs): 11 | self.app_names.append(title) 12 | self.apps.append({ 13 | "title": title, 14 | "function": func, 15 | "args":args, 16 | "kwargs": kwargs 17 | }) 18 | 19 | def run(self, label='Go To'): 20 | def run_component(props): 21 | value = component_multipage_app(key='multipage_app', **props) 22 | return value 23 | def handle_event(value): 24 | # st.header('Streamlit') 25 | # st.write('Received from component: ', value) 26 | app_choice = value['choice']['state']['action'] if value else self.app_names[0] 27 | app_choice_status = value['choice']['state']['value'] if value else False 28 | if app_choice_status == True: 29 | # run the selected app 30 | app = self.apps[self.app_names.index(app_choice)] 31 | app['function'](app['title'], *app['args'], **app['kwargs']) 32 | else: 33 | st.write('Awaiting selection') 34 | 35 | props = { 36 | 'initial_state': { 37 | 'group_1_header': label 38 | } 39 | } 40 | handle_event(run_component(props)) 41 | 42 | def app1(title, info=None): 43 | st.title(title) 44 | st.write(info) 45 | def app2(title, info=None): 46 | st.title(title) 47 | st.write(info) 48 | def app3(title, info=None): 49 | st.title(title) 50 | st.write(info) 51 | 52 | def main(): 53 | mp = MultiPage() 54 | mp.add_app('Application 1', app1, info='Hello from App 1') 55 | mp.add_app('Application 2', app2, info='Hello from App 2') 56 | mp.add_app('Application 3', app3, info='Hello from App 3') 57 | mp.run('Launch Application') 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | streamlit run multipage_app.py 2 | --------------------------------------------------------------------------------