SaulLu's picture
add component code
3fc2d14
raw
history blame
No virus
5.96 kB
/**
* @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<RenderData>(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
}