diff --git a/README.md b/README.md index 208ea8c1..5f088756 100644 --- a/README.md +++ b/README.md @@ -10,74 +10,102 @@ A Streamlit Component is made out of a Python API and a frontend (built using an A Component can be used in any Streamlit app, can pass data between Python and frontend code, and can optionally be distributed on [PyPI](https://pypi.org/) for the rest of the world to use. -- Create a component's API in a single line of Python: - - ```python - import streamlit.components.v1 as components - - # Declare the component: - my_component = components.declare_component("my_component", path="frontend/build") - - # Use it: - my_component(greeting="Hello", name="World") - ``` - -- Build the component's frontend out of HTML and JavaScript (or TypeScript, or ClojureScript, or whatever you fancy). React is supported, but not required: - - ```tsx - import React from 'react'; - import { - withStreamlitConnection, - ComponentProps, - } from 'streamlit-component-lib'; - - function MyComponent({ args }: ComponentProps) { - // Access arguments from Python via `props.args`: - const { greeting, name } = args; - return ( -
- {greeting}, {name}! -
- ); - } - - export default withStreamlitConnection(MyComponent); - ``` - -## Quickstart - -- Ensure you have [Python 3.9+](https://www.python.org/downloads/), [Node.js](https://nodejs.org), and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. -- Clone this repo. -- Create a new Python virtual environment for the template: - ```bash - $ cd template - $ python3 -m venv venv # create venv - $ . venv/bin/activate # activate venv - $ pip install streamlit # install streamlit - ``` -- Initialize and run the component template frontend: - ```bash - $ cd template/my_component/frontend - $ npm install # Install npm dependencies - $ npm run start # Start the Vite dev server - ``` -- From a separate terminal, run the template's Streamlit app: - ```bash - $ cd template - $ . venv/bin/activate # activate the venv you created earlier - $ pip install -e . # install template as editable package - $ streamlit run my_component/example.py # run the example - ``` -- If all goes well, you should see something like this: - ![Quickstart Success](quickstart.png) -- Modify the frontend code at `my_component/frontend/src/MyComponent.tsx`. -- Modify the Python code at `my_component/__init__.py`. +```python +import streamlit as st -## Examples +# Declare the component +my_component = st.components.v2.component( + "your-package.your_component", + js="index-*.js", + html='
', +) + +# Use it directly or via a small wrapper +value = my_component(data={"name": "World"}, default={"num_clicks": 0}) +``` + +```tsx +import { Component } from '@streamlit/component-v2-lib'; +import { createRoot } from 'react-dom/client'; + +const MyComponent: Component = (args) => { + const root = createRoot(args.parentElement.querySelector('.react-root')); + root.render(
Hello, {args.data.name}!
); +}; + +export default MyComponent; +``` + +See full examples in `templates/v2/template/` and `templates/v2/template-reactless/`. + +
+Show v1 code samples + +```python +import streamlit.components.v1 as components + +# Declare the component (v1) +my_component = components.declare_component("my_component", path="frontend/build") + +# Use it +value = my_component(name="World", default=0) +``` + +```tsx +import { + withStreamlitConnection, + ComponentProps, +} from 'streamlit-component-lib'; + +function MyComponent({ args }: ComponentProps) { + return
Hello, {args.name}!\n
; +} + +export default withStreamlitConnection(MyComponent); +``` + +
+ +See full examples in `templates/v1/template/` and `templates/v1/template-reactless/`. -See the `template-reactless` directory for a template that does not use [React](https://reactjs.org/). +## Supported template versions + +This repo provides templates for both Streamlit Component APIs: + +- v2: Uses `st.components.v2.component()` and `@streamlit/component-v2-lib`. +- v1: Uses `st.components.v1.component()` and `streamlit-component-lib`. + +## Quickstart (generate a new component with Cookiecutter) + +- Ensure you have [uv](https://docs.astral.sh/uv/), [Node.js](https://nodejs.org), and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. +- Generate from this template using Cookiecutter via `uvx`: + + - v2 (recommended): + + ```bash + uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v2 + ``` + + - v1: + ```bash + uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v1 + ``` + +- Follow the interactive prompts to generate your project. +- Once created, follow the `README` in your new project to get started. + +## Just browsing? Pre-generated outputs + +If you want to quickly explore without running Cookiecutter, browse the pre-generated templates in this repo: + +- v2 outputs: `templates/v2/template/` and `templates/v2/template-reactless/` +- v1 outputs: `templates/v1/template/` and `templates/v1/template-reactless/` + +From within one of these folders, you can inspect the Python package layout and the `my_component/frontend` code. Refer to the template-level `README` for build instructions. + +## Examples -See the `examples` directory for examples on working with pandas DataFrames, integrating with third-party libraries, and more. +See the `examples/v1/` directory for examples working with pandas DataFrames, integrating with third-party libraries, and more. ## Community-provided Templates diff --git a/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md b/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md index bcdf2856..76d37c3d 100644 --- a/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md +++ b/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md @@ -8,6 +8,14 @@ uv pip install {{ cookiecutter.package_name }} ``` +### Development install (editable) + +When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`: + +```sh +uv pip install -e . --force-reinstall +``` + ## Usage instructions ```python diff --git a/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py b/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py deleted file mode 100644 index a3fdaf8d..00000000 --- a/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py +++ /dev/null @@ -1,84 +0,0 @@ -from pathlib import Path - -import pytest -from e2e_utils import StreamlitRunner -from playwright.sync_api import Page, expect - -ROOT_DIRECTORY = Path(__file__).parent.parent.absolute() -BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py" - - -@pytest.fixture(autouse=True, scope="module") -def streamlit_app(): - with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner: - yield runner - - -@pytest.fixture(autouse=True, scope="function") -def go_to_app(page: Page, streamlit_app: StreamlitRunner): - page.goto(streamlit_app.server_url) - # Wait for app to load - page.get_by_role("img", name="Running...").is_hidden() - - -def test_should_render_template(page: Page): - frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0) - frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - st_markdown_0 = page.get_by_role("paragraph").nth(0) - st_markdown_1 = page.get_by_role("paragraph").nth(1) - - expect(st_markdown_0).to_contain_text("You've clicked 0 times!") - - frame_0.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 0 times!") - - frame_1.get_by_role("button", name="Click me!").click() - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 2 times!") - - page.get_by_label("Enter a name").click() - page.get_by_label("Enter a name").fill("World") - page.get_by_label("Enter a name").press("Enter") - - expect(frame_1.get_by_text("Hello, World!")).to_be_visible() - - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 3 times!") - - -def test_should_change_iframe_height(page: Page): - frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible() - - locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1) - - page.wait_for_timeout(1000) - init_frame_height = locator.bounding_box()["height"] - assert init_frame_height != 0 - - page.get_by_label("Enter a name").click() - - page.get_by_label("Enter a name").fill(35 * "Streamlit ") - page.get_by_label("Enter a name").press("Enter") - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible() - - page.wait_for_timeout(1000) - frame_height = locator.bounding_box()["height"] - assert frame_height > init_frame_height - - page.set_viewport_size({"width": 150, "height": 150}) - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport() - - page.wait_for_timeout(1000) - frame_height_after_viewport_change = locator.bounding_box()["height"] - assert frame_height_after_viewport_change > frame_height diff --git a/quickstart.png b/quickstart.png deleted file mode 100644 index be8f9aa3..00000000 Binary files a/quickstart.png and /dev/null differ diff --git a/templates/v2/template-reactless/README.md b/templates/v2/template-reactless/README.md index 94e710e5..f60ab3dd 100644 --- a/templates/v2/template-reactless/README.md +++ b/templates/v2/template-reactless/README.md @@ -8,6 +8,14 @@ Streamlit component that allows you to do X uv pip install streamlit-custom-component ``` +### Development install (editable) + +When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`: + +```sh +uv pip install -e . --force-reinstall +``` + ## Usage instructions ```python diff --git a/templates/v2/template-reactless/e2e/test_template.py b/templates/v2/template-reactless/e2e/test_template.py deleted file mode 100644 index a3fdaf8d..00000000 --- a/templates/v2/template-reactless/e2e/test_template.py +++ /dev/null @@ -1,84 +0,0 @@ -from pathlib import Path - -import pytest -from e2e_utils import StreamlitRunner -from playwright.sync_api import Page, expect - -ROOT_DIRECTORY = Path(__file__).parent.parent.absolute() -BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py" - - -@pytest.fixture(autouse=True, scope="module") -def streamlit_app(): - with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner: - yield runner - - -@pytest.fixture(autouse=True, scope="function") -def go_to_app(page: Page, streamlit_app: StreamlitRunner): - page.goto(streamlit_app.server_url) - # Wait for app to load - page.get_by_role("img", name="Running...").is_hidden() - - -def test_should_render_template(page: Page): - frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0) - frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - st_markdown_0 = page.get_by_role("paragraph").nth(0) - st_markdown_1 = page.get_by_role("paragraph").nth(1) - - expect(st_markdown_0).to_contain_text("You've clicked 0 times!") - - frame_0.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 0 times!") - - frame_1.get_by_role("button", name="Click me!").click() - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 2 times!") - - page.get_by_label("Enter a name").click() - page.get_by_label("Enter a name").fill("World") - page.get_by_label("Enter a name").press("Enter") - - expect(frame_1.get_by_text("Hello, World!")).to_be_visible() - - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 3 times!") - - -def test_should_change_iframe_height(page: Page): - frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible() - - locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1) - - page.wait_for_timeout(1000) - init_frame_height = locator.bounding_box()["height"] - assert init_frame_height != 0 - - page.get_by_label("Enter a name").click() - - page.get_by_label("Enter a name").fill(35 * "Streamlit ") - page.get_by_label("Enter a name").press("Enter") - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible() - - page.wait_for_timeout(1000) - frame_height = locator.bounding_box()["height"] - assert frame_height > init_frame_height - - page.set_viewport_size({"width": 150, "height": 150}) - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport() - - page.wait_for_timeout(1000) - frame_height_after_viewport_change = locator.bounding_box()["height"] - assert frame_height_after_viewport_change > frame_height diff --git a/templates/v2/template/README.md b/templates/v2/template/README.md index 94e710e5..f60ab3dd 100644 --- a/templates/v2/template/README.md +++ b/templates/v2/template/README.md @@ -8,6 +8,14 @@ Streamlit component that allows you to do X uv pip install streamlit-custom-component ``` +### Development install (editable) + +When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`: + +```sh +uv pip install -e . --force-reinstall +``` + ## Usage instructions ```python diff --git a/templates/v2/template/e2e/test_template.py b/templates/v2/template/e2e/test_template.py deleted file mode 100644 index a3fdaf8d..00000000 --- a/templates/v2/template/e2e/test_template.py +++ /dev/null @@ -1,84 +0,0 @@ -from pathlib import Path - -import pytest -from e2e_utils import StreamlitRunner -from playwright.sync_api import Page, expect - -ROOT_DIRECTORY = Path(__file__).parent.parent.absolute() -BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py" - - -@pytest.fixture(autouse=True, scope="module") -def streamlit_app(): - with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner: - yield runner - - -@pytest.fixture(autouse=True, scope="function") -def go_to_app(page: Page, streamlit_app: StreamlitRunner): - page.goto(streamlit_app.server_url) - # Wait for app to load - page.get_by_role("img", name="Running...").is_hidden() - - -def test_should_render_template(page: Page): - frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0) - frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - st_markdown_0 = page.get_by_role("paragraph").nth(0) - st_markdown_1 = page.get_by_role("paragraph").nth(1) - - expect(st_markdown_0).to_contain_text("You've clicked 0 times!") - - frame_0.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 0 times!") - - frame_1.get_by_role("button", name="Click me!").click() - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 2 times!") - - page.get_by_label("Enter a name").click() - page.get_by_label("Enter a name").fill("World") - page.get_by_label("Enter a name").press("Enter") - - expect(frame_1.get_by_text("Hello, World!")).to_be_visible() - - frame_1.get_by_role("button", name="Click me!").click() - - expect(st_markdown_0).to_contain_text("You've clicked 1 times!") - expect(st_markdown_1).to_contain_text("You've clicked 3 times!") - - -def test_should_change_iframe_height(page: Page): - frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1) - - expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible() - - locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1) - - page.wait_for_timeout(1000) - init_frame_height = locator.bounding_box()["height"] - assert init_frame_height != 0 - - page.get_by_label("Enter a name").click() - - page.get_by_label("Enter a name").fill(35 * "Streamlit ") - page.get_by_label("Enter a name").press("Enter") - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible() - - page.wait_for_timeout(1000) - frame_height = locator.bounding_box()["height"] - assert frame_height > init_frame_height - - page.set_viewport_size({"width": 150, "height": 150}) - - expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport() - - page.wait_for_timeout(1000) - frame_height_after_viewport_change = locator.bounding_box()["height"] - assert frame_height_after_viewport_change > frame_height