├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── screenshot.png ├── setup.py └── streamlit_toggle ├── __init__.py └── frontend ├── .env ├── .prettierrc ├── package-lock.json ├── package.json ├── public ├── bootstrap.min.css └── index.html ├── src ├── StreamlitToggle.tsx ├── index.tsx ├── react-app-env.d.ts └── streamlit │ ├── ArrowTable.ts │ ├── StreamlitReact.tsx │ ├── index.tsx │ └── streamlit.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | streamlit_toggle/frontend/node_modules/ 2 | .venv/ 3 | .vscode/ 4 | *.code-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Sam Dobson 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 streamlit_toggle/frontend/build * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A toggle switch widget for Streamlit 2 | 3 | ![Toggle Switch](screenshot.png?raw=true "Streamlit Toggle Switch") 4 | 5 | ## Usage 6 | 7 | ```python 8 | import streamlit as st 9 | from streamlit_toggle import st_toggleswitch 10 | 11 | awesomeness_enabled = st_toggleswitch("Enable awesomeness") 12 | if awesomeness_enabled: 13 | st.write("Awesomeness has been enabled!") 14 | ``` 15 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdobson/streamlit-toggle/cb78455eb843e6176baf6609f2292be60c48066c/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="streamlit-toggle", 5 | version="1.0.0", 6 | author="Sam Dobson", 7 | author_email="1309834+samdobson@users.noreply.github.com", 8 | description="Toggle widget for Streamlit", 9 | long_description="", 10 | long_description_content_type="text/plain", 11 | url="https://github.com/samdobson/streamlit-toggle", 12 | packages=setuptools.find_packages(), 13 | include_package_data=True, 14 | classifiers=[], 15 | python_requires=">=3.6", 16 | install_requires=[ 17 | "streamlit >= 0.63", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /streamlit_toggle/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit.components.v1 as components 3 | 4 | _RELEASE = False 5 | if not _RELEASE: 6 | _component_func = components.declare_component( 7 | "st_toggleswitch", 8 | url="http://localhost:3001", 9 | ) 10 | else: 11 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 12 | build_dir = os.path.join(parent_dir, "frontend/build") 13 | _component_func = components.declare_component("st_toggleswitch", path=build_dir) 14 | 15 | def st_toggleswitch(label, value=True, disabled=False, key=None): 16 | """Display a toggle switch. 17 | 18 | Parameters 19 | ---------- 20 | label: str 21 | A short label explaining to the user what this toggle switch is for. 22 | value: bool 23 | Default the toggle switch to activated when it first renders 24 | (True if not specified). 25 | disabled: bool 26 | Disable the widget so that it cannot be changed 27 | (False if not specified). 28 | key: str or None 29 | An optional key that uniquely identifies this component. If this is 30 | None, and the component's arguments are changed, the component will 31 | be re-mounted in the Streamlit frontend and lose its current state. 32 | 33 | Returns 34 | ------- 35 | bool 36 | Whether or not the toggle switch is activated. 37 | """ 38 | component_value = _component_func(label=label, value=value, disabled=disabled, key=key, default=value) 39 | return component_value 40 | 41 | if not _RELEASE: 42 | import streamlit as st 43 | 44 | st.subheader("Toggle Switch Streamlit Component") 45 | awesomeness_enabled = st_toggleswitch("Enable awesomeness") 46 | if awesomeness_enabled: 47 | st.markdown("Awesomeness has been enabled!") -------------------------------------------------------------------------------- /streamlit_toggle/frontend/.env: -------------------------------------------------------------------------------- 1 | # Run the component's dev server on :3001 2 | # (The Streamlit dev server already runs on :3000) 3 | PORT=3001 4 | 5 | # Don't automatically open the web browser on `npm run start`. 6 | BROWSER=none 7 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_component_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/hoist-non-react-statics": "^3.3.1", 10 | "@types/jest": "^24.0.0", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "@types/styletron-engine-atomic": "^1.1.0", 15 | "@types/styletron-react": "^5.0.2", 16 | "@types/styletron-standard": "^2.0.0", 17 | "apache-arrow": "^0.17.0", 18 | "baseui": "^9.85.0", 19 | "bootstrap": "^4.4.1", 20 | "event-target-shim": "^5.0.1", 21 | "hoist-non-react-statics": "^3.3.2", 22 | "react": "^16.13.1", 23 | "react-dom": "^16.13.1", 24 | "react-scripts": "3.4.1", 25 | "styletron-engine-atomic": "^1.4.6", 26 | "styletron-react": "^5.2.7", 27 | "typescript": "~3.7.2" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "homepage": "." 51 | } 52 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/StreamlitToggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react" 2 | import { Checkbox, STYLE_TYPE } from 'baseui/checkbox'; 3 | import { 4 | withStreamlitConnection, 5 | StreamlitComponentBase, 6 | Streamlit, 7 | } from "./streamlit" 8 | 9 | interface State { 10 | value: boolean 11 | } 12 | 13 | class StreamlitToggle extends StreamlitComponentBase { 14 | public state = { value: this.props.args["value"] } 15 | 16 | public render = (): ReactNode => { 17 | 18 | return ( 19 |
20 | 26 | 27 | {this.props.args["label"]} 28 |
29 | ) 30 | } 31 | 32 | private onChange = (e: React.ChangeEvent): void => { 33 | const isChecked = e.currentTarget.checked 34 | this.setState({ value: isChecked }, () => { 35 | Streamlit.setComponentValue(isChecked) 36 | }) 37 | } 38 | } 39 | 40 | export default withStreamlitConnection(StreamlitToggle) 41 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import StreamlitToggle from "./StreamlitToggle" 4 | import {Client as Styletron} from 'styletron-engine-atomic'; 5 | import {Provider as StyletronProvider} from 'styletron-react'; 6 | import {ThemeProvider, LightTheme} from 'baseui'; 7 | 8 | const engine = new Styletron(); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ) 20 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/streamlit/ArrowTable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018-2019 Streamlit Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { Table, Type } from "apache-arrow" 19 | 20 | type CellType = "blank" | "index" | "columns" | "data" 21 | 22 | export interface ArrowDataframeProto { 23 | data: ArrowTableProto 24 | height: string 25 | width: string 26 | } 27 | 28 | export interface ArrowTableProto { 29 | data: Uint8Array 30 | index: Uint8Array 31 | columns: Uint8Array 32 | styler: Styler 33 | } 34 | 35 | interface Cell { 36 | classNames: string 37 | content: string 38 | id?: string 39 | type: CellType 40 | } 41 | 42 | interface Styler { 43 | caption?: string 44 | displayValuesTable: Table 45 | styles?: string 46 | uuid: string 47 | } 48 | 49 | export class ArrowTable { 50 | private readonly dataTable: Table 51 | private readonly indexTable: Table 52 | private readonly columnsTable: Table 53 | private readonly styler?: Styler 54 | 55 | constructor( 56 | dataBuffer: Uint8Array, 57 | indexBuffer: Uint8Array, 58 | columnsBuffer: Uint8Array, 59 | styler?: any 60 | ) { 61 | this.dataTable = Table.from(dataBuffer) 62 | this.indexTable = Table.from(indexBuffer) 63 | this.columnsTable = Table.from(columnsBuffer) 64 | this.styler = styler 65 | ? { 66 | caption: styler.get("caption"), 67 | displayValuesTable: Table.from(styler.get("displayValues")), 68 | styles: styler.get("styles"), 69 | uuid: styler.get("uuid"), 70 | } 71 | : undefined 72 | } 73 | 74 | get rows(): number { 75 | return this.indexTable.length + this.columnsTable.numCols 76 | } 77 | 78 | get columns(): number { 79 | return this.indexTable.numCols + this.columnsTable.length 80 | } 81 | 82 | get headerRows(): number { 83 | return this.rows - this.dataRows 84 | } 85 | 86 | get headerColumns(): number { 87 | return this.columns - this.dataColumns 88 | } 89 | 90 | get dataRows(): number { 91 | return this.dataTable.length 92 | } 93 | 94 | get dataColumns(): number { 95 | return this.dataTable.numCols 96 | } 97 | 98 | get uuid(): string | undefined { 99 | return this.styler && this.styler.uuid 100 | } 101 | 102 | get caption(): string | undefined { 103 | return this.styler && this.styler.caption 104 | } 105 | 106 | get styles(): string | undefined { 107 | return this.styler && this.styler.styles 108 | } 109 | 110 | get table(): Table { 111 | return this.dataTable 112 | } 113 | 114 | get index(): Table { 115 | return this.indexTable 116 | } 117 | 118 | get columnTable(): Table { 119 | return this.columnsTable 120 | } 121 | 122 | public getCell = (rowIndex: number, columnIndex: number): Cell => { 123 | const isBlankCell = 124 | rowIndex < this.headerRows && columnIndex < this.headerColumns 125 | const isIndexCell = 126 | rowIndex >= this.headerRows && columnIndex < this.headerColumns 127 | const isColumnsCell = 128 | rowIndex < this.headerRows && columnIndex >= this.headerColumns 129 | 130 | if (isBlankCell) { 131 | const classNames = ["blank"] 132 | if (columnIndex > 0) { 133 | classNames.push("level" + rowIndex) 134 | } 135 | 136 | return { 137 | type: "blank", 138 | classNames: classNames.join(" "), 139 | content: "", 140 | } 141 | } else if (isColumnsCell) { 142 | const dataColumnIndex = columnIndex - this.headerColumns 143 | const classNames = [ 144 | "col_heading", 145 | "level" + rowIndex, 146 | "col" + dataColumnIndex, 147 | ] 148 | 149 | return { 150 | type: "columns", 151 | classNames: classNames.join(" "), 152 | content: this.getContent(this.columnsTable, dataColumnIndex, rowIndex), 153 | } 154 | } else if (isIndexCell) { 155 | const dataRowIndex = rowIndex - this.headerRows 156 | const classNames = [ 157 | "row_heading", 158 | "level" + columnIndex, 159 | "row" + dataRowIndex, 160 | ] 161 | 162 | return { 163 | type: "index", 164 | id: `T_${this.uuid}level${columnIndex}_row${dataRowIndex}`, 165 | classNames: classNames.join(" "), 166 | content: this.getContent(this.indexTable, dataRowIndex, columnIndex), 167 | } 168 | } else { 169 | const dataRowIndex = rowIndex - this.headerRows 170 | const dataColumnIndex = columnIndex - this.headerColumns 171 | const classNames = [ 172 | "data", 173 | "row" + dataRowIndex, 174 | "col" + dataColumnIndex, 175 | ] 176 | const content = this.styler 177 | ? this.getContent( 178 | this.styler.displayValuesTable, 179 | dataRowIndex, 180 | dataColumnIndex 181 | ) 182 | : this.getContent(this.dataTable, dataRowIndex, dataColumnIndex) 183 | 184 | return { 185 | type: "data", 186 | id: `T_${this.uuid}row${dataRowIndex}_col${dataColumnIndex}`, 187 | classNames: classNames.join(" "), 188 | content, 189 | } 190 | } 191 | } 192 | 193 | public getContent = ( 194 | table: Table, 195 | rowIndex: number, 196 | columnIndex: number 197 | ): any => { 198 | const column = table.getColumnAt(columnIndex) 199 | if (column === null) { 200 | return "" 201 | } 202 | 203 | const columnTypeId = this.getColumnTypeId(table, columnIndex) 204 | switch (columnTypeId) { 205 | case Type.Timestamp: { 206 | return this.nanosToDate(column.get(rowIndex)) 207 | } 208 | default: { 209 | return column.get(rowIndex) 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * Returns apache-arrow specific typeId of column. 216 | */ 217 | private getColumnTypeId(table: Table, columnIndex: number): Type { 218 | return table.schema.fields[columnIndex].type.typeId 219 | } 220 | 221 | private nanosToDate(nanos: number): Date { 222 | return new Date(nanos / 1e6) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/streamlit/StreamlitReact.tsx: -------------------------------------------------------------------------------- 1 | import hoistNonReactStatics from "hoist-non-react-statics" 2 | import React, { ReactNode } from "react" 3 | import { RenderData, Streamlit } from "./streamlit" 4 | 5 | /** 6 | * Props passed to custom Streamlit components. 7 | */ 8 | export interface ComponentProps { 9 | /** Named dictionary of arguments passed from Python. */ 10 | args: any 11 | 12 | /** The component's width. */ 13 | width: number 14 | 15 | /** 16 | * True if the component should be disabled. 17 | * All components get disabled while the app is being re-run, 18 | * and become re-enabled when the re-run has finished. 19 | */ 20 | disabled: boolean 21 | } 22 | 23 | /** 24 | * Optional Streamlit React-based component base class. 25 | * 26 | * You are not required to extend this base class to create a Streamlit 27 | * component. If you decide not to extend it, you should implement the 28 | * `componentDidMount` and `componentDidUpdate` functions in your own class, 29 | * so that your plugin properly resizes. 30 | */ 31 | export class StreamlitComponentBase extends React.PureComponent< 32 | ComponentProps, 33 | S 34 | > { 35 | public componentDidMount(): void { 36 | // After we're rendered for the first time, tell Streamlit that our height 37 | // has changed. 38 | Streamlit.setFrameHeight() 39 | } 40 | 41 | public componentDidUpdate(): void { 42 | // After we're updated, tell Streamlit that our height may have changed. 43 | Streamlit.setFrameHeight() 44 | } 45 | } 46 | 47 | /** 48 | * Wrapper for React-based Streamlit components. 49 | * 50 | * Bootstraps the communication interface between Streamlit and the component. 51 | */ 52 | export function withStreamlitConnection( 53 | WrappedComponent: React.ComponentType 54 | ): React.ComponentType { 55 | interface WrapperProps {} 56 | 57 | interface WrapperState { 58 | renderData?: RenderData 59 | componentError?: Error 60 | } 61 | 62 | class ComponentWrapper extends React.PureComponent< 63 | WrapperProps, 64 | WrapperState 65 | > { 66 | public constructor(props: WrapperProps) { 67 | super(props) 68 | this.state = { 69 | renderData: undefined, 70 | componentError: undefined, 71 | } 72 | } 73 | 74 | /** 75 | * Error boundary function. This will be called if our wrapped 76 | * component throws an error. We store the caught error in our state, 77 | * and display it in the next render(). 78 | */ 79 | public static getDerivedStateFromError = ( 80 | error: Error 81 | ): Partial => { 82 | return { componentError: error } 83 | } 84 | 85 | public componentDidMount = (): void => { 86 | // Set up event listeners, and signal to Streamlit that we're ready. 87 | // We won't render the component until we receive the first RENDER_EVENT. 88 | Streamlit.events.addEventListener( 89 | Streamlit.RENDER_EVENT, 90 | this.onRenderEvent 91 | ) 92 | Streamlit.setComponentReady() 93 | } 94 | 95 | public componentDidUpdate = (): void => { 96 | // If our child threw an error, we display it in render(). In this 97 | // case, the child won't be mounted and therefore won't call 98 | // `setFrameHeight` on its own. We do it here so that the rendered 99 | // error will be visible. 100 | if (this.state.componentError != null) { 101 | Streamlit.setFrameHeight() 102 | } 103 | } 104 | 105 | public componentWillUnmount = (): void => { 106 | Streamlit.events.removeEventListener( 107 | Streamlit.RENDER_EVENT, 108 | this.onRenderEvent 109 | ) 110 | } 111 | 112 | /** 113 | * Streamlit is telling this component to redraw. 114 | * We save the render data in State, so that it can be passed to the 115 | * component in our own render() function. 116 | */ 117 | private onRenderEvent = (event: Event): void => { 118 | // Update our state with the newest render data 119 | const renderEvent = event as CustomEvent 120 | this.setState({ renderData: renderEvent.detail }) 121 | } 122 | 123 | public render = (): ReactNode => { 124 | // If our wrapped component threw an error, display it. 125 | if (this.state.componentError != null) { 126 | return ( 127 |
128 |

Component Error

129 | {this.state.componentError.message} 130 |
131 | ) 132 | } 133 | 134 | // Don't render until we've gotten our first RENDER_EVENT from Streamlit. 135 | if (this.state.renderData == null) { 136 | return null 137 | } 138 | 139 | return ( 140 | 145 | ) 146 | } 147 | } 148 | 149 | return hoistNonReactStatics(ComponentWrapper, WrappedComponent) 150 | } 151 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/streamlit/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018-2020 Streamlit Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // Workaround for type-only exports: 19 | // https://stackoverflow.com/questions/53728230/cannot-re-export-a-type-when-using-the-isolatedmodules-with-ts-3-2-2 20 | import { ComponentProps as ComponentProps_ } from "./StreamlitReact" 21 | import { RenderData as RenderData_ } from "./streamlit" 22 | 23 | export { 24 | StreamlitComponentBase, 25 | withStreamlitConnection, 26 | } from "./StreamlitReact" 27 | export { ArrowTable } from "./ArrowTable" 28 | export { Streamlit } from "./streamlit" 29 | export type ComponentProps = ComponentProps_ 30 | export type RenderData = RenderData_ -------------------------------------------------------------------------------- /streamlit_toggle/frontend/src/streamlit/streamlit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018-2020 Streamlit Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // Safari doesn't support the EventTarget class, so we use a shim. 19 | import { EventTarget } from "event-target-shim" 20 | import { ArrowDataframeProto, ArrowTable } from "./ArrowTable" 21 | 22 | /** Data sent in the custom Streamlit render event. */ 23 | export interface RenderData { 24 | args: any 25 | disabled: boolean 26 | } 27 | 28 | /** Messages from Component -> Streamlit */ 29 | enum ComponentMessageType { 30 | // A component sends this message when it's ready to receive messages 31 | // from Streamlit. Streamlit won't send any messages until it gets this. 32 | // Data: { apiVersion: number } 33 | COMPONENT_READY = "streamlit:componentReady", 34 | 35 | // The component has a new widget value. Send it back to Streamlit, which 36 | // will then re-run the app. 37 | // Data: { value: any } 38 | SET_COMPONENT_VALUE = "streamlit:setComponentValue", 39 | 40 | // The component has a new height for its iframe. 41 | // Data: { height: number } 42 | SET_FRAME_HEIGHT = "streamlit:setFrameHeight", 43 | } 44 | 45 | /** 46 | * Streamlit communication API. 47 | * 48 | * Components can send data to Streamlit via the functions defined here, 49 | * and receive data from Streamlit via the `events` property. 50 | */ 51 | export class Streamlit { 52 | /** 53 | * The Streamlit component API version we're targetting. 54 | * There's currently only 1! 55 | */ 56 | public static readonly API_VERSION = 1 57 | 58 | public static readonly RENDER_EVENT = "streamlit:render" 59 | 60 | /** Dispatches events received from Streamlit. */ 61 | public static readonly events = new EventTarget() 62 | 63 | private static registeredMessageListener = false 64 | private static lastFrameHeight?: number 65 | 66 | /** 67 | * Tell Streamlit that the component is ready to start receiving data. 68 | * Streamlit will defer emitting RENDER events until it receives the 69 | * COMPONENT_READY message. 70 | */ 71 | public static setComponentReady = (): void => { 72 | if (!Streamlit.registeredMessageListener) { 73 | // Register for message events if we haven't already 74 | window.addEventListener("message", Streamlit.onMessageEvent) 75 | Streamlit.registeredMessageListener = true 76 | } 77 | 78 | Streamlit.sendBackMsg(ComponentMessageType.COMPONENT_READY, { 79 | apiVersion: Streamlit.API_VERSION, 80 | }) 81 | } 82 | 83 | /** 84 | * Report the component's height to Streamlit. 85 | * This should be called every time the component changes its DOM - that is, 86 | * when it's first loaded, and any time it updates. 87 | */ 88 | public static setFrameHeight = (height?: number): void => { 89 | if (height === undefined) { 90 | // `height` is optional. If undefined, it defaults to scrollHeight, 91 | // which is the entire height of the element minus its border, 92 | // scrollbar, and margin. 93 | height = document.body.scrollHeight 94 | } 95 | 96 | if (height === Streamlit.lastFrameHeight) { 97 | // Don't bother updating if our height hasn't changed. 98 | return 99 | } 100 | 101 | Streamlit.lastFrameHeight = height 102 | Streamlit.sendBackMsg(ComponentMessageType.SET_FRAME_HEIGHT, { height }) 103 | } 104 | 105 | /** 106 | * Set the component's value. This value will be returned to the Python 107 | * script, and the script will be re-run. 108 | * 109 | * For example: 110 | * 111 | * JavaScript: 112 | * Streamlit.setComponentValue("ahoy!") 113 | * 114 | * Python: 115 | * value = st.my_component(...) 116 | * st.write(value) # -> "ahoy!" 117 | * 118 | * The value must be serializable into JSON. 119 | */ 120 | public static setComponentValue = (value: any): void => { 121 | Streamlit.sendBackMsg(ComponentMessageType.SET_COMPONENT_VALUE, { value }) 122 | } 123 | 124 | /** Receive a ForwardMsg from the Streamlit app */ 125 | private static onMessageEvent = (event: MessageEvent): void => { 126 | const type = event.data["type"] 127 | switch (type) { 128 | case Streamlit.RENDER_EVENT: 129 | Streamlit.onRenderMessage(event.data) 130 | break 131 | } 132 | } 133 | 134 | /** 135 | * Handle an untyped Streamlit render event and redispatch it as a 136 | * StreamlitRenderEvent. 137 | */ 138 | private static onRenderMessage = (data: any): void => { 139 | let args = data["args"] 140 | if (args == null) { 141 | console.error( 142 | `Got null args in onRenderMessage. This should never happen` 143 | ) 144 | args = {} 145 | } 146 | 147 | // Parse our dataframe arguments with arrow, and merge them into our args dict 148 | const dataframeArgs = 149 | data["dfs"] && data["dfs"].length > 0 150 | ? Streamlit.argsDataframeToObject(data["dfs"]) 151 | : {} 152 | 153 | args = { 154 | ...args, 155 | ...dataframeArgs, 156 | } 157 | 158 | const disabled = Boolean(data["disabled"]) 159 | 160 | // Dispatch a render event! 161 | const eventData = { disabled, args } 162 | const event = new CustomEvent(Streamlit.RENDER_EVENT, { 163 | detail: eventData, 164 | }) 165 | Streamlit.events.dispatchEvent(event) 166 | } 167 | 168 | private static argsDataframeToObject = ( 169 | argsDataframe: ArgsDataframe[] 170 | ): object => { 171 | const argsDataframeArrow = argsDataframe.map( 172 | ({ key, value }: ArgsDataframe) => [key, Streamlit.toArrowTable(value)] 173 | ) 174 | return Object.fromEntries(argsDataframeArrow) 175 | } 176 | 177 | private static toArrowTable = (df: ArrowDataframeProto): ArrowTable => { 178 | const { data, index, columns } = df.data 179 | return new ArrowTable(data, index, columns) 180 | } 181 | 182 | /** Post a message to the Streamlit app. */ 183 | private static sendBackMsg = (type: string, data?: any): void => { 184 | window.parent.postMessage( 185 | { 186 | isStreamlitMessage: true, 187 | type: type, 188 | ...data, 189 | }, 190 | "*" 191 | ) 192 | } 193 | } 194 | 195 | interface ArgsDataframe { 196 | key: string 197 | value: ArrowDataframeProto 198 | } 199 | -------------------------------------------------------------------------------- /streamlit_toggle/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------