© Mohammad Khorasani, Mohamed Abdou, Javier Hernández Fernández 2022
M. Khorasani et al.Web Application Development with Streamlithttps://doi.org/10.1007/978-1-4842-8111-6_10

10. Building Streamlit Components

Mohammad Khorasani1  , Mohamed Abdou2 and Javier Hernández Fernández1
(1)
Doha, Qatar
(2)
Cambridge, United Kingdom
 

Streamlit regularly extends its features to incorporate additional capabilities that can be utilized by developers in a few short lines of code. At some point however, developers will need to take matters into their own hands and build something tailored to address their own specific user requirements. Whether this is to modify the user interface or to offer a bespoke experience to the user, Streamlit allows the developer to create custom components on demand. Furthermore, Streamlit supports using ReactJS as a wingman to build components that can be used as a live web server or built using deployment-ready code. In this chapter, we will walk through the steps required to make new Streamlit components, to use them in a Pythonic context, and to share them with the general public.

10.1 Introduction to Streamlit Custom Components

Fundamentally, Streamlit is a backend server, serving web page changes to client browsers using DG. HTML and JavaScript can be generated by any web frameworks. Hence, Streamlit can help serving components from any web application framework. Web application frameworks can be quite advanced like ReactJS, where the developer codes in JSX but builds the application to give out a combination of files that can be served statically from disk. In a production environment, it is highly advised to have static files served from disk as illustrated in Figure 10-1. However, Streamlit still allows including components that are hosted locally, but as a trade-off, this component imported in the Streamlit application will share different features from the Streamlit app. For instance, if you print the current URL from the custom component, it will not give out the same URL which the Streamlit application is hosted on.
Figure 10-1

How custom components are served through Streamlit

10.2 Using ReactJS to Create Streamlit Custom Components

In this section, we will showcase how to make a ReactJS-based component to be used in Streamlit. Along with that, we will also demonstrate how to share data bidirectionally between Streamlit and the component. This can be used to send initial values, user action triggers, or even styling themes and colors to the custom component.

10.2.1 Making a ReactJS Component

Initially, Node and npm needs to be installed from NodeJS.org. Then, we will use Streamlit’s official template for making custom components from GitHub. In this subsection, we will quickly cover making a ReactJS component.

In this example we will produce a rating stars widget as shown in Figure 10-2 in ReactJS and build upon it in the following subsections. The developer community of ReactJS is considerably larger than that of other frameworks, making it worthwhile to invest time in creating such components. .
Figure 10-2

Interactive rating star view from Material UI

Copying the content of component-template/template/my_component to our working directory will set up a ReactJS application with a one file module, which is in src/MyComponent.tsx, and that is the file that needs to be modified in order to achieve our goal of making a rating star component. Modifying the file will result in Listing 10-1
import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React, { ReactNode } from "react"
interface State {}
class MyComponent extends StreamlitComponentBase<State> {
  public state = {}
  public render = (): ReactNode => {
   return (
    <div></div>
   )
  }
}
export default withStreamlitConnection(MyComponent)
Listing 10-1

stars_demo/rating_stars/frontend/src/CleanedTemplate.tsx

Use the following command to install the Material UI library:
npm i @mui/material
Then run the following command to install other packages in the package.json file:
npm i
After running both commands, make sure to change the name in package.json to be your component’s reference name in Streamlit down the road as shown in Listing 10-2.
{
   "name": "rating_star",
   "version": "0.1.0",
   "private": true,
   "dependencies": {
     "@mui/material": "ˆ5.0.6",
     "...
   },
   "scripts": ...,
   "eslintConfig": ...,
   "browserslist": ...,
   "homepage": "."
}
Listing 10-2

Updated Package.json

Having the necessary packages ready, and with a little JavaScript/TypeScript knowledge, or with a little bit of Googling, we can start making our first ReactJS module to be used as a custom Streamlit component. We have to modify Listing 10-1 to display the rating stars as documented in Material UI’s site. As an end result, the new file content will be as shown in Listing 10-3 which should reside in frontend/src/. And don’t forget to change the file to be run in the index.tsx.
import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React, { ReactNode } from "react"
import { Rating } from '@mui/material';
interface State {}
class RatingStar extends StreamlitComponentBase<State> {
  public state = {}
  public render = (): ReactNode => {
   return (
     <Rating size="large" defaultValue={3} />
   )
  }
}
export default withStreamlitConnection(RatingStar)
Listing 10-3

Initial version of RatingStar.tsx

10.2.2 Using a ReactJS Component in Streamlit

To get the React application ready, run the following command. If errors popped up mentioning a missing package, install it using the command mentioned before.
npm start
Now, on Streamlit’s side, we will use the already running ReactJS application as a component, by leveraging Streamlit’s API to include external components. But first, we need to populate the __init__.py as shown in Listing 10-4.
import os
import streamlit.components.v1 as components
IS_RELEASE = False
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("rating_stars", path=build_path)
else:
   _component_func = components.declare_component("rating_stars", url="http://localhost:3001")
def rating_stars():
   _component_func()
Listing 10-4

Initial version of __init__.py

The previous code snippet uses a live component running locally on port 3001, which in our case needs to be the ReactJS app. Then it exposes a function to be used by any other Python source to be run as a Streamlit module. Executing Listing 10-5 with Streamlit's CLI tool, will result in what is shown in Figure 10-3.
import streamlit as st
from rating_stars import rating_stars
st.title("Rating stars demo!")
rating_stars()
Listing 10-5

Initial version of main.py

Figure 10-3

First custom component!

That being made, we were able to display a live ReactJS application in a Streamlit context, but if we become more creative and make more custom components, it will be a hassle to go to the folder of each and run it as a ReactJS application before running the Streamlit application. Maybe even run out of ports as each custom component has its own unique local URL. Hence, we can overcome this issue by building the ReactJS application into static files, after it is been developed, by running
npm run build

Now a new folder called build will appear in the frontend folder, containing the necessary JavaScript, CSS, and HTML files to be used by Streamlit to load it into an application. Once that is done and it is fixed to run the built version of a component, we need to change the IS_RELEASE to True, forcing Streamlit to load the new custom component from the frontend/build/ folder. And that is exactly what is being conveyed by Figure 10-1.

10.2.3 Sending Data to the Custom Component

At this point, we can just display ReactJS applications in a Streamlit context, albeit without a communication mechanism between the front and back ends. Now, we will showcase how to send data from Streamlit to ReactJS, when the transferable data is dynamic, giving us the capability of sending information from Streamlit with every rerender to the ReactJS application.

As a step toward making our rating star custom component more useful, we will add the support of setting the total star count and how many of them are selected, all from Streamlit’s Python code. First, we need to understand that Streamlit converts Python’s passed parameter of _component_func to ReactJS’s properties. So, our goal is to refactor the component’s __init__.py file to allow the new parameters as shown in Listing 10-6 and read them from RatingStar.tsx and then place them in the view’s properties like Listing 10-7.
import os
import streamlit.components.v1 as components
IS_RELEASE = False
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("rating_stars", path=build_path)
else:
   _component_func = components.declare_component("rating_stars", url="http://localhost:3001")
def rating_stars(stars_count: int, selected: int):
   _component_func(stars_count=stars_count, selected=selected)
Listing 10-6

Second version of __init__.py

import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React, { ReactNode } from "react"
import { Rating } from "@mui/material"
interface State {}
class RatingStar extends StreamlitComponentBase<State> {
  public state = {}
  public render = (): ReactNode => {
    const {selected, stars_count} = this.props.args
    return <Rating size="large" defaultValue={selected} max={stars_count}/>
  }
}
export default withStreamlitConnection(RatingStar)
Listing 10-7

Second version of RatingStar.tsx

Bringing this new update to action, we will make the Streamlit application as shown in Listing 10-8 to showcase native and custom components side by side. Figure 10-4 shows the homogeneity between our newly made configurable component and out-of-the-box components by Streamlit.
import streamlit as st
from rating_stars import rating_stars
st.title("Rating stars demo!")
total_stars = st.slider(label="Total Stars", min_value=0, max_value=20, value=10, step=1) selected_stars = st.slider(label="Selected Stars", min_value=0, max_value=total_stars, step=1) rating_stars(total_stars, selected_stars)
Listing 10-8

Second version of main.py

Figure 10-4

Using native and custom components

10.2.4 Receiving Data from the Custom Component

After playing around with the sliders to set our custom rating component, we can notice how our Streamlit application’s view is bloated with extra widgets that are used to set another view’s behavior, when we already could have set the number of selected stars by hovering the mouse to and clicking any star. However, this will require the Streamlit application to know what value is selected, and here comes a way to get data from custom components in Streamlit.

Streamlit didn’t only make an API to include a ReactJS application in its context but also communicate bidirectionally with it. Sending data out of ReactJS can be made using a library already included in the template’s package.json. Using this library, we can trigger many ReactJS component–specific actions, but we will only shed the light on the function Streamlit.setComponentValue, which makes the component’s return value in a Streamlit’s Python context the same as what is fed to its first parameter. Knowing this, we will add it to the callback of the rating view in ReactJS as shown in Listing 10-9. Then accordingly change the component’s __init__.py content to be as shown in Listing 10-10 to forward the return value back to our ran Streamlit file.
import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React, { ReactNode } from "react"
import { Rating } from "@mui/material"
interface State {}
class RatingStar extends StreamlitComponentBase<State> {
  public state = {}
  public render = (): ReactNode => {
    const { selected, stars_count } = this.props.args
    return (
     <Rating
       size="large"
       defaultValue={1}
       max={stars_count}
       onChange={(_, stars_count) => Streamlit.setComponentValue(stars_count)}
     />
    )
  }
}
export default withStreamlitConnection(RatingStar)
Listing 10-9

Final version of RatingStar.tsx

import os
import streamlit.components.v1 as components
IS_RELEASE = False
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("rating_stars", path=build_path)
else:
   _component_func = components.declare_component("rating_stars", url="http://localhost:3001")
def rating_stars(stars_count: int):
   stars_selected = _component_func(stars_count=stars_count)
   if stars_selected is None:
     stars_selected = 0
   return stars_selected
Listing 10-10

Final version of __init__.py

Now in our main Streamlit file, we remove the slider for the selected star count and then replace it with the output of the custom component. And finally, we display on our application the number of stars selected, as shown in Listing 10-11, with the output shown in Figure 10-5.
import streamlit as st
from rating_stars import rating_stars
st.title("Rating stars demo!")
total_stars = st.slider(label="Total Stars", min_value=0, max_value=20, value=10, step=1) selected_stars = rating_stars(total_stars)
st.write(str(selected_stars) + " star(s) have been selected")
Listing 10-11

main.py, Streamlit file to be run

Figure 10-5

Result of communicating back and forth with a custom Streamlit component

10.3 Publishing Components As Pip Packages

Once you finally make your first custom component, you will want to share it with your friends or dedicate it to the open source developer community like many do. Sending a zip file of the source code or uploading the component to a version control service like GitHub might not be a scalable option to reach unlimited number of developers with ease, as this adds more overhead for them to get it running.

A more developer-friendly yet professional approach to share Python packages specifically is to compress them in a pip wheel format, which then can be easily installed in the interpreter by running
pip install <PIP_PACKAGE_NAME>.whl

Following up with the continuous example in this chapter, we don’t need to install any new package to make this possible as Python supports wheel building out of the box. The goal is to package the rating_stars/ folder in a file which then can be installed and referenced from any script like it is a local package.

Building a pip file is as simple as running the following command:
python setup.py sdist bdist_wheel
Before making the pip wheel, make sure to build the ReactJS part of the custom component, as it will not be run live on the user end; rather, it should be a seamless plug-and-play experience using that new component. After navigating to the rating_stars/frontend/, run
npm run build
However, the wheel builder needs more information about the exact folder to package and other miscellaneous metadata such as version number and description. No need to mention the folder to be packaged, as Python looks for all “Python Packages” in the current folder, and for a folder to be considered as a Python package, it needs to have an __init__.py file, which we already have. However, by default the wheel builder doesn’t include non-Python files and folders if they don’t have a single Python file. This is an issue in our case as our component relies on the ReactJS build folder which contains all static web files necessary for it to run. To overcome this issue, a new file as shown in Listing 10-12 needs to be added in the project’s root folder with its content to force inclusion of the build folder and its content.
recursive-include rating_stars/frontend/build *
Listing 10-12

MANIFEST.in

As we now have half of the requirements to make a pip wheel, we can tackle the final part by making a file called setup.py with the content shown in Listing 10-13 in the same folder as the MANIFEST.in. The setup file can include the version number of your custom component, description, and other information such as its pip download name if uploaded to pypi.​org.
import setuptools
setuptools.setup(
    name="rating_stars",
    version="0.1",
    author="YOUR-NAME",
    author_email="[email protected]",
    description="INSERT-DESCRIPTION-HERE",
    long_description="INSERT-LONGER-DESCRIPTION-HERE",
    packages=setuptools.find_packages(),
    include_package_data=True,
    classifiers=[
       "Programming Language :: Python :: 3",
       "License :: OSI Approved :: MIT License",
       "Operating System :: OS Independent",
    ],
    keywords=['Python', 'Streamlit', 'React', 'JavaScript', 'Custom'],
    python_requires=">=3.6",
    install_requires=[
       "streamlit >= 0.86",
    ],
)
Listing 10-13

setup.​py

After finally running the Python package command, we can find three new folders appearing in the project’s root folder as seen in Figure 10-6. We are interested in the rating_stars-0.1-py3-none-any.whl file in the second folder; this file can be sent to anyone and installed easily as long as the package requirements are met.

Other created folders can have some benefits as well. For instance, the dist/ folder can be used by twine, which is the package and tool used to upload pip wheels to the global pip repository. If interested in sharing your package with the general public, sign up to pypi.org and then run the following command:
python -m twine upload dist/* --verbose
after building the wheel to upload it.
Figure 10-6

New folders after building the custom component

10.4 Component in Focus: Extra-Streamlit-Components

Streamlit as a framework is constantly evolving, which means it may not offer certain bespoke features that you may need for a production-ready web application. Features such as application routing and having custom URLs for multiple views or saving user-related data on the browser side. Apart from our usability requirements, sometimes it may just be necessary to offer a unique look and feel for the application or to add a widget which is not natively supported by Streamlit. Previously, we learned how to make a simple custom component; however, with components the sky is truly the limit, and in this chapter, we will showcase Extra-Streamlit-Components (STX), an open source collection of intricate Streamlit components and services. In addition, we will explain how every subcomponent is built from the Streamlit and ReactJS perspective, and hopefully creative developers will be inspired enough to unleash components of their own.

10.4.1 Stepper Bar

This is inspired by Material UI’s Stepper. As mentioned before, ReactJS’s developer community withholds various useful components that can be imported into the Streamlit world without a big hassle. And this stepper bar can actually be useful in the context of most Streamlit application, as it allows moving through sequential steps in a specific order to do something which is mostly data science related. It is a simple component as it returns the index of the stage the user has arrived to, as seen in Figures 10-7, 10-8, and 10-9. You, as a developer, are not bound to only three phases; rather, you can supply a list of all tab names, and the return value will be the selected list item index as might be shown in Listing 10-15. Numbering and animations are already taken care of.
Figure 10-7

Stepper bar phase 3

Figure 10-8

Stepper bar phase 3

Figure 10-9

Stepper bar phase 3

The ReactJS side of this component requires installing the stepper package through npm, then importing it in the source file as shown in Listing 10-14. This file is responsible in detecting user clicks and returning the equivalent level index, as well as managing each step’s theme depending on where the user currently is.
import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React from "react"
import { withStyles, createStyles } from "@material-ui/core/styles"
import Stepper from "@material-ui/core/Stepper"
import Step from "@material-ui/core/Step"
import StepLabel from "@material-ui/core/StepLabel"
const styles = createStyles((theme) => ({
   root: {
     width: "100%",
     backgroundColor: "transparent",
   },
   icon: {
     color: "grey",
     cursor: "pointer",
     "&$activeIcon": {
       color: "#f63366",
      },
     "&$completedIcon": {
       color: "#f63366",
     },
   },
   activeIcon: {},
   completedIcon: {},
}))
class StepperBar extends StreamlitComponentBase {
   state = { activeStep: 0, steps: [] }
   componentDidMount() {
     this.setState((prev, state) => ({
       steps: this.props.args.steps,
       activeStep: this.props.args.default,
     }))
   }
   onClick = (index) => {
     const { activeStep } = this.state
     if (index == activeStep + 1) {
       this.setState(
         (prev, state) => ({
            activeStep: activeStep + 1,
          }),
          () => Streamlit.setComponentValue(this.state.activeStep)
       )
     } else if (index < activeStep) {
       this.setState(
         (prev, state) => ({
           activeStep: index,
         }),
         () => Streamlit.setComponentValue(this.state.activeStep)
       )
     }
   }
   getLabelStyle = (index) => {
     const { activeStep } = this.state
     const style = {}
     if (index == activeStep) {
         style.color = "#f63366"
         style.fontStyle = "italic"
      }  else if (index < activeStep) {
         style.color = "#f63366"
         style.fontWeight = "bold"
       }  else {
         style.color = "grey"
      }
      return style
   }
   render = () => {
     let { classes } = this.props
     const { activeStep } = this.state
     const steps = this.state.steps
     return (
       <div className={classes.root}>
         <Stepper
            activeStep={activeStep}
            alternativeLabel
            className={classes.root}
          >
            {steps.map((label, index) => (
             <Step key={label} onClick={() => this.onClick(index)}>
               <StepLabel
                  StepIconProps={{
                     classes: {
                       cursor: "pointer",
                       root: classes.icon,
                       active: classes.activeIcon,
                       completed: classes.completedIcon,
                     },
                  }}
               >
                 <p style={this.getLabelStyle(index)}>{label}</p>
               </StepLabel>
             </Step>
            ))}
        </Stepper>
      </div>
    )
  }
}
export default withStreamlitConnection(withStyles(styles)(StepperBar))
Listing 10-14

StepperBar/frontend/src/StepperBar.jsx

import os
import streamlit.components.v1 as components
from streamlit.components.v1.components import CustomComponent
from typing import List
from extra_streamlit_components import IS_RELEASE
if IS_RELEASE:
    absolute_path = os.path.dirname(os.path.abspath(__file__))
    build_path = os.path.join(absolute_path, "frontend/build")
    _component_func = components.declare_component("stepper_bar", path=build_path)
else:
    _component_func = components.declare_component("stepper_bar", url="http://localhost:3001")
def stepper_bar(steps: List[str]) -> CustomComponent:
    component_value = _component_func(steps=steps, default=0)
    return component_value
Listing 10-15

StepperBar/__init__.py

10.4.2 Bouncing Image

This component offers zooming animations for an image with a bouncing effect. It can be used in loading moments or splash screen. This might not be a frequently used component, but when it does, the animation duration, control switch, and dimensions are needed for it to work based on different requirements as seen in Listing 10-16. The ReactJS aspect of it is a little more complex than thePythonic aspect, as it needs to manage animation cycles and return back the status which requires setting state and reporting widget state back to Streamlit every cycle. Even though JavaScript is not the main focus of this book, Listing 10-17 is relatively simple to understand. However, the final result shall look something like Figure 10-10.
import os
import streamlit.components.v1 as components
from extra_streamlit_components import IS_RELEASE
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("bouncing_image", path=build_path)
else:
   _component_func = components.declare_component("bouncing_image", url="http://localhost:3001")
def bouncing_image(image_source: str, animate: bool, animation_time: int, height: float, width: float):
   _component_func(image=image_source, animate=animate, animation_time=animation_time, height=height, width=width)
Listing 10-16

BouncingImage/__init__.py

import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React from "react"
import { withStyles, createStyles } from "@material-ui/core/styles"
import Grow from "@material-ui/core/Grow"
import CardMedia from "@material-ui/core/CardMedia"
const styles = createStyles((theme) => ({
   root: {
     height: 180,
   },
   container: {
     display: "flex",
   },
   paper: {
     margin: 1,
   },
   svg: {
     width: 100,
     height: 100,
   },
   polygon: {
     fill: "white",
     stroke: "red",
     strokeWidth: 1,
   },
}))
class BouncingImage extends StreamlitComponentBase {
   state = {
     animationTimeRoundTrip: 1750,
     isAnimating: true,
     keepAnimating: false,
   }
   constructor(props) {
     super(props)
   }
   componentDidMount() {
     const { animation_time, animate } = this.props.args
     Streamlit.setComponentValue(animate)
     this.setState(
       () => ({
         animationTimeRoundTrip: animation_time,
         keepAnimating: animate,
       }),
       () =>
         setInterval(
           () =>
             this.state.keepAnimating &&
             this.setState(
               () => ({
                 isAnimating:
                   !this.state.isAnimating && this.state.keepAnimating,
               }),
               () => Streamlit.setComponentValue(this.state.keepAnimating)
             ),
           this.state.animationTimeRoundTrip / 2
          )
      )
   }
   render = () => {
     const isAnimating = this.state.isAnimating
     let {
       classes,
       args: { image, height, width },
     } = this.props
     return (
       <div className={classes.root}>
         <div className={classes.container}>
           <Grow
             in={isAnimating}
             style={{ transformOrigin: "0 0 0" }}
             {...(isAnimating
               ? { timeout: this.state.animationTimeRoundTrip / 2 }
               : {})}
           >
             <CardMedia image={image} style={{ height, width }} />
          </Grow>
        </div>
       </div>
      )
     }
   }
export default withStreamlitConnection(withStyles(styles)(BouncingImage))
Listing 10-17

BouncingImage/frontend/src/BouncingImage.jsx

Figure 10-10

Bouncing image demo (a snapshot from the zoom animation)

10.4.3 Tab Bar

Instead of making a Streamlit column widget to host multiple buttons which will act as a tab bar, you can just use this custom component. It provides a way to encapsulate the title, description, and ID of each button in a UI-organized way as it provides a horizontal scroll view if the tabs – side-to-side length – exceeded the window’s width.

Figures 10-11 and 10-12 show the behavior of the tab button once it is clicked and the output of the component in Streamlit. Creating those tabs requires passing a list of specific Python objects as shown in Listing 10-19 which will then be parsed to JSON and be processed by the TypeScript ReactJS component in Listing 10-18.
import {
   Streamlit,
   StreamlitComponentBase,
   withStreamlitConnection,
}  from "streamlit-component-lib"
import React, { ComponentProps, ReactNode } from "react"
import ScrollMenu from "react-horizontal-scrolling-menu"
interface State {
   numClicks: number
   selectedId: number
}
interface MenuItem {
   id: number
   title: string
   description: string
}
class TabBar extends StreamlitComponentBase<State> {
public state = { numClicks: 0, selectedId: 1, list: [] }
  constructor(props: ComponentProps<any>) {
    super(props)
    this.state.list = this.props.args["data"]
    this.state.selectedId = this.props.args["selectedId"]
  }
   MenuItem = ({ item, selectedId }: { item: MenuItem; selectedId: number }) => {
     return (
       <div className={'menu-item ${selectedId == item.id ? "active" : ""}'}>
         <div>{item.title}</div>
         <div style={{ fontWeight: "normal", fontStyle: "italic" }}>
            {item.description}
         </div>
       </div>
     )
   }
   Menu(list: Array<MenuItem>, selectedId: number) {
     return list.map((item) => (
       <this.MenuItem item={item} selectedId={selectedId} key={item.id} />
     ))
   }
   Arrow = ({ text, className }: { text: string; className: string }) => {
     return <div className={className}>{text}</div>
   }
   ArrowLeft = this.Arrow({ text: "<", className: "arrow-prev" })
   ArrowRight = this.Arrow({ text: ">", className: "arrow-next" })
   public render = (): ReactNode => {
     return (
       <div>
         <ScrollMenu
            alignCenter={false}
            data={this.Menu(this.state.list, this.state.selectedId)}
            wheel={true}
            scrollToSelected={true}
            selected={'${this.state.selectedId}'}
            onSelect={this.onSelect}
         />
         <hr
            style={{
              borderColor: "var(--streamlit-primary-color)",
            }}
         />
       </div>
     )
   }
   onSelect = (id: any) => {
     this.setState(
       (state, props) => {
         return { selectedId: id }
       },
       () => Streamlit.setComponentValue(id)
     )
   }
}
export default withStreamlitConnection(TabBar)
Listing 10-18

TabBar/frontend/src/TabBar.tsx

import os
import streamlit.components.v1 as components
from dataclasses import dataclass
from typing import List
from extra_streamlit_components import IS_RELEASE
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("tab_bar", path=build_path)
else:
   _component_func = components.declare_component("tab_bar", url="http://localhost:3001")
@dataclass(frozen=True, order=True, unsafe_hash=True)
class TabBarItemData:
   id: int
   title: str
   description: str
   def to_dict(self):
       return {"id": self.id, "title": self.title, "description": self.description}
def tab_bar(data: List[TabBarItemData], default=None, return_type=str, key=None):
   data = list(map(lambda item: item.to_dict(), data))
   component_value = _component_func(data=data, selectedId=default, key=key, default=default)
   try:
       if return_type == str:
           return str(component_value)
       elif return_type == int:
           return int(component_value)
       elif return_type == float:
           return float(component_value)
       except:
           return component_value
Listing 10-19

TabBar/__init__.py

Figure 10-11

Tab bar with first element selected

Figure 10-12

Tab bar with first element selected

10.4.4 Cookie Manager

This has been introduced before in the previous chapter; however, it was treated as a black box which can just save data we think of on the client browser side. The Cookie Manager is not only a custom component but also a Python service, as it relies on data managing in a Pythonic context with CRUD operations on a ReactJS-based component. If you are a web developer, you might already know that setting cookies is not a big deal, as you are writing code to be executed on the client side. Fortunately, in Streamlit we can write both server-side and client-side code in the same script. Knowing that we can make React custom component which will then be executed on the browser, we can set it up to control cookies on the client’s end, as shown in Figure 10-13.
Figure 10-13

Using Streamlit to control client-side data

Leveraging the knowledge from the material introduced in this chapter, we can set up a bidirectional communication between Streamlit’s server end and the custom ReactJS component running on the client’s browser. By doing so, we can command our component to gather, delete, or add cookies on the browser and even listen to the return value, if any. Starting with the ReactJS side in Listing 10-20, we first read the expected arguments such as the operation needed and then the data to act on, then based on what is asked we take actions against the cookies using the npm’s package universal-cookie, and finally we send back a response about the status of the operation.

On Python’s end, Listing 10-21 just encapsulates the whole communication method with the browser’s component. Also, it stores all the cookies in memory for that user once initialized, in order to save more network traffic time. However, if the class’s constructor is not cached once first initialized, it will have no added value as it will be executed every time Streamlit reruns. That is why it is advised to use the snippet in Listing 10-22 when using the Cookie Manager. Figure 10-14 shows a demo of this custom component.
import {
  Streamlit,
  ComponentProps,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { useEffect, useState } from "react"
import Cookies from "universal-cookie"
let last_output = null
const cookies = new Cookies()
const CookieManager = (props: ComponentProps) => {
  const setCookie = (cookie, value, expires_at) => {
    cookies.set(cookie, value, {
      path: "/",
      samesite: "strict",
      expires: new Date(expires_at),
    })
    return true
  }
  const getCookie = (cookie) => {
    const value = cookies.get(cookie)
    return value
  }
  const deleteCookie = (cookie) => {
    cookies.remove(cookie, { path: "/", samesite: "strict" })
    return true
  }
  const getAllCookies = () => {
    return cookies.getAll()
  }
  const { args } = props
  const method = args["method"]
  const cookie = args["cookie"]
  const value = args["value"]
  const expires_at = args["expires_at"]
  let output = null
  switch (method) {
    case "set":
      output = setCookie(cookie, value, expires_at)
      break
    case "get":
      output = getCookie(cookie)
      break
    case "getAll":
      output = getAllCookies()
      break
    case "delete":
      output = deleteCookie(cookie)
      break
    default:
      break
  }
  if (output && JSON.stringify(last_output) != JSON.stringify(output)) {
    last_output = output
    Streamlit.setComponentValue(output)
    Streamlit.setComponentReady()
  }
  useEffect(() => Streamlit.setFrameHeight())
     return <div></div>
}
export default withStreamlitConnection(CookieManager)
Listing 10-20

CookieManager/frontend/src/CookieManager.tsx

import os
import streamlit.components.v1 as components
import datetime
from extra_streamlit_components import IS_RELEASE
if IS_RELEASE:
   absolute_path = os.path.dirname(os.path.abspath(__file__))
   build_path = os.path.join(absolute_path, "frontend/build")
   _component_func = components.declare_component("cookie_manager", path=build_path)
else:
   _component_func = components.declare_component("cookie_manager",
      url="http://localhost:3001")
class CookieManager:
   def __init__(self, key="init"):
      self.cookie_manager = _component_func
      self.cookies = self.cookie_manager(method="getAll", key=key, default={})
   def get(self, cookie: str):
      return self.cookies.get(cookie)
   def set(self, cookie, val,
      expires_at=datetime.datetime.now() + datetime.timedelta(days=1), key="set"):
      if cookie is None or cookie == "":
          return
      expires_at = expires_at.isoformat()
      did_add = self.cookie_manager(method="set", cookie=cookie, value=val, expires_at=expires_at, key=key, default=False)
      if did_add:
         self.cookies[cookie] = val
   def delete(self, cookie, key="delete"):
      if cookie is None or cookie == "":
          return
      did_add = self.cookie_manager(method="delete", cookie=cookie, key=key, default=False)
      if did_add:
         del self.cookies[cookie]
   def get_all(self, key="get_all"):
     self.cookies = self.cookie_manager(method="getAll", key=key, default={})
     return self.cookies
Listing 10-21

CookieManager/__init__.py

@st.cache(allow_output_mutation=True)
def get_manager():
    return stx.CookieManager()
cookie_manager = get_manager()
Listing 10-22

How to initialize and use Cookie Manager

Figure 10-14

Cookie Manager demo from Extra-Streamlit-Components

10.4.5 Router

In almost any web application, different URLs exist. This helps in providing two main benefits, modularization of content’s code to the developer and an easy access for the user to specific pages. This STX component is different than its counterparts, as it doesn’t make a ReactJS application, rather uses Python and Streamlit tools to make use of query parameters for routing.

Query parameters is used as Streamlit – as of version 1.1.0 – didn’t support URL path modification, as the application by default loads at root http://<DOMAIN>:<PORT>/. However, query parameters are manipulable, as you set the key-value pairs in the URL like http://<DOMAIN>:<PORT>/?key1=v1&k2=value2 by using the native Streamlit function:
st.experimental_set_query_params(key1="v1", k2="value2")
And get them in dictionary format by
st.experimental_get_query_params() # {"key1":"v1", "k2":"value2"}
As we can control the query parameters, nothing is stopping us from mimicking a URL behavior with it, to make something like this: http://<DOMAIN>:<PORT>/?nav=/electronics/computers. To the untrained eye, this looks like any other URL, but it technically works differently by achieving the same goal. Listing 10-23 of this component shows usage of session state along with query parameters as Streamlit’s query parameter has some time delay causing problems in navigating between different routes as it is still experimental as of that time. It also calls the view’s passed function or class whenever it is asked to by calling show_route_view which checks the current route and if there is an equivalent callable function. Usage of this module can be referred to Listing 10-24.
import streamlit as st
from urllib.parse import unquote
import time
def does_support_session_state():
   try:
       return st.session_state is not None
   except:
       return False
class Router:
   def __init__(self, routes: dict, **kwargs):
     self.routes = routes
     if "key" in kwargs:
         st.warning("No need for a key for initialization,"
                    " this is not a rendered component.")
     if not does_support_session_state():
         raise Exception(
             "Streamlit installation doesn't support session state."
             " Session state needs to be available in the used Streamlit installation")
   def show_route_view(self):
      query_route = self.get_nav_query_param()
      sys_route = self.get_url_route()
      if sys_route is None and query_route is None:
          self.route("/")
          return
      elif sys_route is not None and query_route is not None:
          st.experimental_set_query_params(nav=sys_route)
          st.session_state['stx_router_route'] = sys_route
      elif query_route is not None:
          self.route(query_route)
          return
      _callable = self.routes.get(sys_route)
      if callable(_callable):
         _callable()
   def get_nav_query_param(self):
      url = st.experimental_get_query_params().get("nav")
      url = url[0] if type(url) == list else url
      route = unquote(url) if url is not None else url
      return route
   def get_url_route(self):
      if "stx_router_route" in st.session_state and
              st.session_state.stx_router_route is not None:
          return st.session_state.stx_router_route
      route = self.get_nav_query_param()
      return route
   def route(self, new_route):
      if new_route[0] != "/":
           new_route = "/" + new_route
      st.session_state['stx_router_route'] = new_route
      st.experimental_set_query_params(nav=new_route)
      time.sleep(0.1) # Needed for URL param refresh
      st.experimental_rerun()
Listing 10-23

Router/__init__.py

@st.cache(allow_output_mutation=True, hash_funcs={"_thread.RLock": lambda _: None})
def init_router():
   return stx.Router({"/home": home, "/landing": landing})
def home():
   return st.write("This is a home page")
def landing():
   return st.write("This is the landing page")
router = init_router()
router.show_route_view()
c1, c2, c3 = st.columns(3)
with c1:
   st.header("Current route")
   current_route = router.get_url_route()
   st.write(f"{current_route}")
with c2:
   st.header("Set route")
   new_route = st.text_input("route")
   if st.button("Route now!"):
      router.route(new_route)
with c3:
   st.header("Session state")
   st.write(st.session_state)
Listing 10-24

Demo usage of STX’s router

Trying the demo out with multiple routes, we can notice that it acts as expected where the routes having the nav parameter set to either /landing or /home show the equivalent page content as seen in Figures 10-16 and 10-17, respectively. We can also see the root route returns nothing in Figure 10-15, and the same behavior occurs in Figure 10-18. This module can be further enhanced by adding a not found route, which is something like the infamous 404 page seen quite often on the Internet.
Figure 10-15

The root route

Figure 10-16

/landing route

10.5 Summary

As you come to the end of this chapter, you are banking on the knowledge needed to render innovative and exciting custom components for Streamlit. By using the simplified version of ReactJS’s template and by referring to online resources, you can pretty much not only clone ReactJS’s Material UI views to Streamlit but also control some browser functionalities to add a native web application UX to your application. In this chapter, we also discussed how certain aspects of the user interface in Streamlit can be customized to add more versatility and exclusivity to an application. The techniques discussed in building this library can indeed be scaled and implemented by any developer for a multitude of other purposes. It is just worth mentioning that no component is done justice without sharing it with the open source community, to improve it iteratively with feedback and suggestions from other developers.
Figure 10-17

/home route

Figure 10-18

A random unknown route

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.162.179