/** * @license * Copyright 2018-2020 Streamlit Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Safari doesn't support the EventTarget class, so we use a shim. import { EventTarget } from "event-target-shim" import { ArrowDataframeProto, ArrowTable } from "./ArrowTable" /** Data sent in the custom Streamlit render event. */ export interface RenderData { args: any disabled: boolean } /** Messages from Component -> Streamlit */ enum ComponentMessageType { // A component sends this message when it's ready to receive messages // from Streamlit. Streamlit won't send any messages until it gets this. // Data: { apiVersion: number } COMPONENT_READY = "streamlit:componentReady", // The component has a new widget value. Send it back to Streamlit, which // will then re-run the app. // Data: { value: any } SET_COMPONENT_VALUE = "streamlit:setComponentValue", // The component has a new height for its iframe. // Data: { height: number } SET_FRAME_HEIGHT = "streamlit:setFrameHeight", } /** * Streamlit communication API. * * Components can send data to Streamlit via the functions defined here, * and receive data from Streamlit via the `events` property. */ export class Streamlit { /** * The Streamlit component API version we're targetting. * There's currently only 1! */ public static readonly API_VERSION = 1 public static readonly RENDER_EVENT = "streamlit:render" /** Dispatches events received from Streamlit. */ public static readonly events = new EventTarget() private static registeredMessageListener = false private static lastFrameHeight?: number /** * Tell Streamlit that the component is ready to start receiving data. * Streamlit will defer emitting RENDER events until it receives the * COMPONENT_READY message. */ public static setComponentReady = (): void => { if (!Streamlit.registeredMessageListener) { // Register for message events if we haven't already window.addEventListener("message", Streamlit.onMessageEvent) Streamlit.registeredMessageListener = true } Streamlit.sendBackMsg(ComponentMessageType.COMPONENT_READY, { apiVersion: Streamlit.API_VERSION, }) } /** * Report the component's height to Streamlit. * This should be called every time the component changes its DOM - that is, * when it's first loaded, and any time it updates. */ public static setFrameHeight = (height?: number): void => { if (height === undefined) { // `height` is optional. If undefined, it defaults to scrollHeight, // which is the entire height of the element minus its border, // scrollbar, and margin. height = document.body.scrollHeight + 10; } if (height === Streamlit.lastFrameHeight) { // Don't bother updating if our height hasn't changed. return } Streamlit.lastFrameHeight = height Streamlit.sendBackMsg(ComponentMessageType.SET_FRAME_HEIGHT, { height }) } /** * Set the component's value. This value will be returned to the Python * script, and the script will be re-run. * * For example: * * JavaScript: * Streamlit.setComponentValue("ahoy!") * * Python: * value = st.my_component(...) * st.write(value) # -> "ahoy!" * * The value must be serializable into JSON. */ public static setComponentValue = (value: any): void => { Streamlit.sendBackMsg(ComponentMessageType.SET_COMPONENT_VALUE, { value }) } /** Receive a ForwardMsg from the Streamlit app */ private static onMessageEvent = (event: MessageEvent): void => { const type = event.data["type"] switch (type) { case Streamlit.RENDER_EVENT: Streamlit.onRenderMessage(event.data) break } } /** * Handle an untyped Streamlit render event and redispatch it as a * StreamlitRenderEvent. */ private static onRenderMessage = (data: any): void => { let args = data["args"] if (args == null) { console.error( `Got null args in onRenderMessage. This should never happen` ) args = {} } // Parse our dataframe arguments with arrow, and merge them into our args dict const dataframeArgs = data["dfs"] && data["dfs"].length > 0 ? Streamlit.argsDataframeToObject(data["dfs"]) : {} args = { ...args, ...dataframeArgs, } const disabled = Boolean(data["disabled"]) // Dispatch a render event! const eventData = { disabled, args } const event = new CustomEvent(Streamlit.RENDER_EVENT, { detail: eventData, }) Streamlit.events.dispatchEvent(event) } private static argsDataframeToObject = ( argsDataframe: ArgsDataframe[] ): object => { const argsDataframeArrow = argsDataframe.map( ({ key, value }: ArgsDataframe) => [key, Streamlit.toArrowTable(value)] ) return Object.fromEntries(argsDataframeArrow) } private static toArrowTable = (df: ArrowDataframeProto): ArrowTable => { const { data, index, columns } = df.data return new ArrowTable(data, index, columns) } /** Post a message to the Streamlit app. */ private static sendBackMsg = (type: string, data?: any): void => { window.parent.postMessage( { isStreamlitMessage: true, type: type, ...data, }, "*" ) } } interface ArgsDataframe { key: string value: ArrowDataframeProto }